From 110bbbd2076b5c03f6bc6ef27e82e4810bc84dcd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 31 May 2020 20:46:15 +1200 Subject: [PATCH 01/86] :zap: Tag linting, plus tons of fixes according to linter's rules --- .eslintrc.json | 198 +- gulpfile.js | 62 +- package-lock.json | 92 +- package.json | 4 + src/js/additionalShellHandiness.js | 10 +- src/js/codeEditorHelpers.js | 644 +++--- src/js/debuggerTools.js | 6 +- src/js/firstRunSetup.js | 28 +- src/js/gulpWatch.js | 28 +- src/js/links.js | 22 +- src/js/loadLanguageJSON.js | 6 +- src/js/loadProject.js | 23 +- src/js/migration/0.3.0.js | 2 +- src/js/migration/1.3.0.js | 9 +- src/js/riotMixins.js | 178 +- src/js/roomCopyTools.js | 98 +- src/js/roomTileTools.js | 314 +-- src/js/userScriptsNotice.js | 2 +- src/js/windowWatcher.js | 18 +- src/js/zoomSetup.js | 4 +- src/node_requires/exporter/css.js | 10 +- src/node_requires/exporter/emitterTandems.js | 4 +- src/node_requires/exporter/fonts.js | 8 +- src/node_requires/exporter/html.js | 13 +- src/node_requires/exporter/icons.js | 28 +- src/node_requires/exporter/index.js | 36 +- src/node_requires/exporter/rooms.js | 6 +- src/node_requires/exporter/skeletons.js | 36 +- src/node_requires/exporter/sounds.js | 10 +- src/node_requires/exporter/styles.js | 4 +- src/node_requires/exporter/textures.js | 277 +-- src/node_requires/exporter/types.js | 11 +- src/node_requires/generateGUID.js | 4 +- src/node_requires/generators/gridTexture.js | 5 +- src/node_requires/hotkeys.js | 16 +- src/node_requires/jellify.js | 12 +- src/node_requires/objectUtils.js | 6 +- src/node_requires/platformUtils.js | 2 +- src/node_requires/resources/textures.js | 50 +- src/node_requires/styleUtils.js | 2 +- src/pug/.eslintrc.json | 5 + src/pug/debuggerToolbar.pug | 2 +- src/pug/includes/head.pug | 24 +- src/pug/index.pug | 147 +- src/pug/qrCodePanel.pug | 2 +- src/riotTags/.eslintrc.json | 5 + src/riotTags/actions-editor.tag | 12 +- src/riotTags/debugger/debugger-modal.tag | 17 +- src/riotTags/debugger/debugger-screen.tag | 40 +- src/riotTags/debugger/debugger-toolbar.tag | 48 +- src/riotTags/export-panel.tag | 90 +- src/riotTags/font-editor.tag | 4 +- src/riotTags/fonts-panel.tag | 82 +- src/riotTags/license-panel.tag | 2 +- src/riotTags/main-menu.tag | 1156 ++++++----- src/riotTags/method-selector.tag | 22 +- src/riotTags/modules-panel.tag | 106 +- src/riotTags/new-project-onboarding.tag | 8 +- src/riotTags/notepad-panel.tag | 249 ++- src/riotTags/particles/emitter-editor.tag | 43 +- .../particles/emitter-tandem-editor.tag | 108 +- src/riotTags/particles/fx-panel.tag | 29 +- src/riotTags/patreon-screen.tag | 50 +- src/riotTags/project-selector.tag | 35 +- .../rooms/room-backgrounds-editor.tag | 16 +- src/riotTags/rooms/room-editor.tag | 1372 ++++++------- src/riotTags/rooms/room-events-editor.tag | 269 +-- src/riotTags/rooms/room-tile-editor.tag | 87 +- src/riotTags/rooms/room-type-picker.tag | 4 +- src/riotTags/rooms/rooms-panel.tag | 441 ++--- src/riotTags/root-tag.tag | 52 +- src/riotTags/script-editor.tag | 10 +- src/riotTags/scripts-panel.tag | 58 +- src/riotTags/settings-panel.tag | 190 +- src/riotTags/shared/asset-viewer.tag | 16 +- src/riotTags/shared/collapsible-section.tag | 2 +- src/riotTags/shared/color-input.tag | 9 +- src/riotTags/shared/color-picker.tag | 24 +- src/riotTags/shared/context-menu.tag | 6 +- src/riotTags/shared/curve-editor.tag | 40 +- src/riotTags/shared/docs-shortcut.tag | 2 +- src/riotTags/shared/texture-input.tag | 4 +- src/riotTags/shared/texture-selector.tag | 10 +- src/riotTags/sound-editor.tag | 170 +- src/riotTags/sounds-panel.tag | 227 +-- src/riotTags/style-editor.tag | 546 ++--- src/riotTags/styles-panel.tag | 282 +-- src/riotTags/texture-editor.tag | 1753 +++++++++-------- src/riotTags/textures-panel.tag | 728 ++++--- src/riotTags/type-editor.tag | 38 +- src/riotTags/types-panel.tag | 21 +- 91 files changed, 5605 insertions(+), 5346 deletions(-) create mode 100644 src/pug/.eslintrc.json create mode 100644 src/riotTags/.eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json index c97f18112..879db276a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,9 +5,16 @@ "es6": true, "node": true }, + "plugins": ["pug"], "globals": { + "alertify": true, + "dragonBones": true, + "monaco": true, "nw": true, - "PIXI": true + "PIXI": true, + "QRCode": true, + "riot": true, + "soundbox": true }, "parserOptions": { "ecmaVersion": 2018 @@ -15,109 +22,69 @@ "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", - "array-bracket-spacing": [ - "error", - "never" - ], + "array-bracket-spacing": ["error", "never"], "array-callback-return": "error", + "array-element-newline": ["error", "consistent"], "arrow-body-style": "error", "arrow-parens": "off", - "arrow-spacing": [ - "error", - { - "after": true, - "before": true - } - ], + "arrow-spacing": ["error", {"after": true, "before": true}], "block-scoped-var": "error", - "block-spacing": "off", - "brace-style": [ - "error", - "1tbs", - { - "allowSingleLine": true - } - ], + "block-spacing": ["error", "never"], + "brace-style": ["error", "1tbs", {"allowSingleLine": false}], "callback-return": "off", "camelcase": "error", - "capitalized-comments": [ - "off", - "always" - ], + "capitalized-comments": ["off", "always"], "class-methods-use-this": "error", - "comma-dangle": "off", - "comma-spacing": "off", - "comma-style": [ - "error", - "last" - ], + "comma-dangle": ["warn", "never"], + "comma-spacing": ["error", {"before": false, "after": true}], + "comma-style": ["error", "last"], "complexity": "error", - "computed-property-spacing": [ - "error", - "never" - ], + "computed-property-spacing": ["error","never"], "consistent-return": "error", "consistent-this": "error", "curly": "error", "default-case": "error", "dot-location": "off", "dot-notation": "error", - "eol-last": "error", + "eol-last": ["error", "always"], "eqeqeq": "error", - "func-call-spacing": "error", + "func-call-spacing": ["error", "never"], "func-name-matching": "error", - "func-names": [ - "error", - "never" - ], - "func-style": [ - "error", - "expression" - ], + "func-names": ["error", "as-needed"], + "func-style": ["error", "expression"], + "function-paren-newline": ["error", "multiline"], "generator-star-spacing": "error", "global-require": "off", "guard-for-in": "off", "handle-callback-err": "error", - "id-blacklist": "error", - "id-length": "off", + "id-blacklist": ["error", "data", "tag", "element"], + "id-length": ["error", {"min": 1, "max": 24}], "id-match": "error", - "indent": "off", + "indent": ["error", 4, {"VariableDeclarator": "first", "MemberExpression": "off", "ignoreComments": true}], "init-declarations": "off", "jsx-quotes": "error", - "key-spacing": "error", - "keyword-spacing": [ - "error", - { - "after": true, - "before": true - } - ], + "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], + "keyword-spacing": ["error", {"after": true, "before": true}], "line-comment-position": "off", - "linebreak-style": [ - "off", - "unix" - ], + "linebreak-style": ["error", "unix"], "lines-around-comment": "off", - "lines-around-directive": "off", - "max-depth": "error", - "max-len": "off", + "max-depth": ["error", 4], + "max-len": ["error", {"code": 100, "ignoreComments": false, "ignoreUrls": true, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true}], "max-lines": "off", - "max-nested-callbacks": "error", - "max-params": "off", + "max-lines-per-function": ["error", {"max": 80, "skipBlankLines": true, "skipComments": true}], + "max-nested-callbacks": ["error", 10], + "max-params": ["error", 5], "max-statements": "off", - "max-statements-per-line": "off", - "multiline-ternary": "off", - "new-cap": "error", + "max-statements-per-line": ["error", {"max": 1}], + "multiline-ternary": ["error", "always-multiline"], + "new-cap": "warn", "new-parens": "error", - "newline-after-var": "off", - "newline-before-return": "off", - "newline-per-chained-call": "error", + "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}], "no-alert": "error", "no-array-constructor": "error", "no-await-in-loop": "error", - "no-bitwise": "error", + "no-bitwise": "warn", "no-caller": "error", - "no-catch-shadow": "error", "no-confusing-arrow": "error", "no-console": ["warn", { "allow": ["error"] @@ -150,15 +117,24 @@ "no-lonely-if": "error", "no-loop-func": "error", "no-magic-numbers": "off", - "no-mixed-operators": "off", + "no-mixed-operators": [ + "error", + { + "groups": [ + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": true + } + ], "no-mixed-requires": "error", "no-mixed-spaces-and-tabs": "error", "no-multi-spaces": "error", "no-multi-str": "error", - "no-multiple-empty-lines": "error", - "no-native-reassign": "error", + "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1}], "no-negated-condition": "off", - "no-negated-in-lhs": "error", "no-nested-ternary": "error", "no-new": "error", "no-new-func": "error", @@ -177,7 +153,7 @@ "no-restricted-imports": "error", "no-restricted-modules": "error", "no-restricted-properties": "error", - "no-restricted-syntax": "error", + "no-restricted-syntax": ["error", "WithStatement"], "no-return-assign": "error", "no-return-await": "error", "no-script-url": "error", @@ -185,18 +161,18 @@ "no-sequences": "error", "no-shadow": "off", "no-shadow-restricted-names": "error", - "no-spaced-func": "error", "no-sync": "off", "no-tabs": "error", "no-template-curly-in-string": "error", "no-ternary": "off", "no-throw-literal": "error", - "no-trailing-spaces": "off", + "no-trailing-spaces": "error", "no-undef-init": "error", "no-undefined": "error", - "no-underscore-dangle": "error", + "no-underscore-dangle": ["error", {"allow": ["__dirname"]}], "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", + "no-unsafe-negation": "error", "no-unused-expressions": "error", "no-use-before-define": "error", "no-useless-call": "error", @@ -211,64 +187,54 @@ "no-warning-comments": "warn", "no-whitespace-before-property": "error", "no-with": "error", - "object-curly-newline": "off", - "object-curly-spacing": "error", - "object-property-newline": "error", + "object-curly-newline": ["error", { + "ObjectExpression": {"minProperties": 1}, + "ObjectPattern": "never", + "ImportDeclaration": "never", + "ExportDeclaration": {"multiline": true, "minProperties": 2} + }], + "object-curly-spacing": ["error", "never"], + "object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}], "object-shorthand": "error", "one-var": "off", - "one-var-declaration-per-line": "error", + "one-var-declaration-per-line": ["error", "initializations"], "operator-assignment": "error", - "operator-linebreak": "off", - "padded-blocks": "off", + "operator-linebreak": ["error", "after"], + "padded-blocks": ["error", "never"], "prefer-arrow-callback": "off", "prefer-const": "error", "prefer-destructuring": "error", + "prefer-exponentiation-operator": "warn", "prefer-numeric-literals": "error", - "prefer-reflect": "off", "prefer-rest-params": "error", "prefer-spread": "error", "prefer-template": "off", - "quote-props": "off", - "quotes": [ - "error", - "single" - ], + "quote-props": ["error", "as-needed"], + "quotes": ["error", "single"], "radix": "error", "require-atomic-updates": "error", "require-await": "error", - "require-jsdoc": "off", "rest-spread-spacing": "error", "semi": ["error", "always"], "semi-spacing": "error", - "sort-imports": "error", + "semi-style": ["error", "last"], + "sort-imports": "off", "sort-keys": "off", "sort-vars": "off", - "space-before-blocks": "off", - "space-before-function-paren": "off", - "space-in-parens": [ - "error", - "never" - ], - "space-infix-ops": "off", + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", "space-unary-ops": "error", - "spaced-comment": "off", + "spaced-comment": "error", "strict": "off", "symbol-description": "error", - "template-curly-spacing": [ - "error", - "never" - ], - "unicode-bom": [ - "error", - "never" - ], - "valid-jsdoc": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], "vars-on-top": "off", - "wrap-regex": "error", + "wrap-regex": "off", "yield-star-spacing": "error", - "yoda": [ - "error", - "never" - ] + "yoda": ["error", "never"] } } diff --git a/gulpfile.js b/gulpfile.js index 3b7bb9c85..b5579178f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -32,7 +32,8 @@ const npm = (/^win/).test(process.platform) ? 'npm.cmd' : 'npm'; const pack = require('./app/package.json'); -var channelPostfix = argv.channel || false; +var channelPostfix = argv.channel || false, + fixEnabled = argv.fix || false; let errorBoxShown = false; const showErrorBox = function () { @@ -109,7 +110,10 @@ const compileRiot = () => .pipe(gulp.dest('./temp/')); const concatScripts = () => - streamQueue({objectMode: true}, + streamQueue( + { + objectMode: true + }, gulp.src('./src/js/3rdparty/riot.min.js'), gulp.src(['./src/js/**', '!./src/js/3rdparty/riot.min.js']), gulp.src('./temp/riot.js') @@ -205,15 +209,31 @@ const lintStylus = () => { const lintJS = () => { const eslint = require('gulp-eslint'); - return gulp.src(['./src/js/**/*.js', '!./src/js/3rdparty/**/*.js', './src/node_requires/**/*.js']) - .pipe(eslint()) + return gulp.src([ + './src/js/**/*.js', + '!./src/js/3rdparty/**/*.js', + './src/node_requires/**/*.js', + './src/pug/**/*.pug' + ]) + .pipe(eslint({ + fix: fixEnabled + })) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}; +const lintTags = () => { + const eslint = require('gulp-eslint'), + replaceExt = require('gulp-ext-replace'); + return gulp.src(['./src/riotTags/**/*.tag']) + .pipe(replaceExt('.pug')) // rename so that it becomes edible for eslint-plugin-pug + .pipe(eslint()) // ESLint-pug cannot automatically fix issues .pipe(eslint.format()) .pipe(eslint.failAfterError()); }; const lintI18n = () => require('./node_requires/i18n')().then(console.log); -const lint = gulp.series(lintJS, lintStylus, lintI18n); +const lint = gulp.series(lintJS, lintTags, lintStylus, lintI18n); const launchApp = () => { const NwBuilder = require('nw-builder'); @@ -224,7 +244,7 @@ const launchApp = () => { flavor: 'sdk' }); return nw.run() - .catch(function (error) { + .catch(error => { showErrorBox(); console.error(error); }) @@ -268,7 +288,7 @@ const getDocumentation = doc => { if (doc.kind === 'function') { return { value: `${doc.description} -${(doc.params || []).map(param => `* \`${param.name}\` (${param.type.names.join('|')}) ${param.description} ${param.optional? '(optional)' : ''}`).join('\n')} +${(doc.params || []).map(param => `* \`${param.name}\` (${param.type.names.join('|')}) ${param.description} ${param.optional ? '(optional)' : ''}`).join('\n')} Returns ${doc.returns[0].type.names.join('|')}, ${doc.returns[0].description}` }; @@ -314,13 +334,11 @@ const concatTypedefs = () => gulp.src(['./src/typedefs/ct.js/types.d.ts', './src/typedefs/ct.js/**/*.ts', './src/typedefs/default/**/*.ts']) .pipe(concat('global.d.ts')) // patch the generated output so ct classes allow custom properties - .pipe(replace( - 'declare class Copy extends PIXI.AnimatedSprite {', ` + .pipe(replace('declare class Copy extends PIXI.AnimatedSprite {', ` declare class Copy extends PIXI.AnimatedSprite { [key: string]: any `)) - .pipe(replace( - 'declare class Room extends PIXI.Container {', ` + .pipe(replace('declare class Room extends PIXI.Container {', ` declare class Room extends PIXI.Container { [key: string]: any `)) @@ -439,8 +457,7 @@ if ((/^win/).test(process.platform)) { const zipsForAllPlatforms = platforms.map(platform => () => gulp.src(`./build/ctjs - v${pack.version}/${platform}/**`) .pipe(zip(`ct.js v${pack.version} for ${platform}.zip`)) - .pipe(gulp.dest(`./build/ctjs - v${pack.version}/`)) - ); + .pipe(gulp.dest(`./build/ctjs - v${pack.version}/`))); zipPackages = gulp.parallel(zipsForAllPlatforms); } else { const execute = require('./node_requires/execute'); @@ -451,8 +468,7 @@ if ((/^win/).test(process.platform)) { execute(({exec}) => exec(` cd "./build/ctjs - v${pack.version}/" zip -rqy "ct.js v${pack.version} for ${platform}.zip" "./${platform}" - `)) - )); + `)))); } @@ -469,13 +485,13 @@ const patronsCache = done => { const dest = './app/data/patronsCache.csv', src = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTUMd6nvY0if8MuVDm5-zMfAxWCSWpUzOc81SehmBVZ6mytFkoB3y9i9WlUufhIMteMDc00O9EqifI3/pub?output=csv'; const file = fs.createWriteStream(dest); - http.get(src, function(response) { + http.get(src, response => { response.pipe(file); - file.on('finish', function() { + file.on('finish', () => { file.close(() => done()); // close() is async, call cb after close completes. }); }) - .on('error', function(err) { // Handle errors + .on('error', err => { // Handle errors fs.unlink(dest); // Delete the file async. (But we don't check the result) done(err); }); @@ -496,11 +512,11 @@ const packages = gulp.series([ const deployOnly = () => { console.log(`For channel ${channelPostfix}`); - return spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/linux32`, `comigo/ct:linux32${channelPostfix? '-' + channelPostfix: ''}`, '--userversion', pack.version]) - .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/linux64`, `comigo/ct:linux64${channelPostfix? '-' + channelPostfix: ''}`, '--userversion', pack.version])) - .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/osx64`, `comigo/ct:osx64${channelPostfix? '-' + channelPostfix: ''}`, '--userversion', pack.version])) - .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/win32`, `comigo/ct:win32${channelPostfix? '-' + channelPostfix: ''}`, '--userversion', pack.version])) - .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/win64`, `comigo/ct:win64${channelPostfix? '-' + channelPostfix: ''}`, '--userversion', pack.version])); + return spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/linux32`, `comigo/ct:linux32${channelPostfix ? '-' + channelPostfix : ''}`, '--userversion', pack.version]) + .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/linux64`, `comigo/ct:linux64${channelPostfix ? '-' + channelPostfix : ''}`, '--userversion', pack.version])) + .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/osx64`, `comigo/ct:osx64${channelPostfix ? '-' + channelPostfix : ''}`, '--userversion', pack.version])) + .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/win32`, `comigo/ct:win32${channelPostfix ? '-' + channelPostfix : ''}`, '--userversion', pack.version])) + .then(() => spawnise.spawn('./butler', ['push', `./build/ctjs - v${pack.version}/win64`, `comigo/ct:win64${channelPostfix ? '-' + channelPostfix : ''}`, '--userversion', pack.version])); }; const deploy = gulp.series([packages, deployOnly]); diff --git a/package-lock.json b/package-lock.json index 2545a9d8b..edffa50f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ctjsbuildenvironment", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -142,6 +142,12 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", + "dev": true + }, "accord": { "version": "0.26.4", "resolved": "https://registry.npmjs.org/accord/-/accord-0.26.4.tgz", @@ -763,8 +769,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" } } }, @@ -2453,6 +2458,20 @@ "resolved": "https://registry.npmjs.org/eslint-config-riot/-/eslint-config-riot-1.0.0.tgz", "integrity": "sha1-+9ZThpgLMPvNDhMF1MP7hhTvIRk=" }, + "eslint-plugin-pug": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-pug/-/eslint-plugin-pug-1.2.2.tgz", + "integrity": "sha512-Xx7W04GUZwsChao7FTYOgmyrKXkpGGYeBw0LRd1mYkhivKhtHkDomanVtyU0KmFZUYhzSDget5462B6cPtnImQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15", + "pug-lexer": "^4.1.0", + "pug-parser": "^5.0.1", + "pug-walk": "^1.1.8", + "vfile": "^4.0.2", + "vfile-location": "^3.0.0" + } + }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", @@ -4243,8 +4262,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" }, "micromatch": { "version": "3.1.10", @@ -4648,6 +4666,15 @@ } } }, + "gulp-ext-replace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gulp-ext-replace/-/gulp-ext-replace-0.3.0.tgz", + "integrity": "sha1-/1xc/LklUNqpIyqPPrNe9Ty18mA=", + "dev": true, + "requires": { + "through2": "~2.0.1" + } + }, "gulp-if": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", @@ -8710,8 +8737,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" } } }, @@ -9692,6 +9718,15 @@ "crypto-random-string": "^1.0.0" } }, + "unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "requires": { + "@types/unist": "^2.0.2" + } + }, "universalify": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", @@ -9881,6 +9916,49 @@ "extsprintf": "^1.2.0" } }, + "vfile": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.0.tgz", + "integrity": "sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + } + } + }, + "vfile-location": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.0.1.tgz", + "integrity": "sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ==", + "dev": true + }, + "vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + } + }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", diff --git a/package.json b/package.json index 25f0f97e1..18779ea53 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,9 @@ "stylint-stylish": "^2.0.0", "stylus": "^0.54.7", "tsd-jsdoc": "^2.5.0" + }, + "devDependencies": { + "eslint-plugin-pug": "^1.2.2", + "gulp-ext-replace": "^0.3.0" } } diff --git a/src/js/additionalShellHandiness.js b/src/js/additionalShellHandiness.js index 4fc1f352f..a524bb9ad 100644 --- a/src/js/additionalShellHandiness.js +++ b/src/js/additionalShellHandiness.js @@ -5,13 +5,15 @@ * @param {boolean} [options.openDirectory] Whether or not to choose a directory instead of files * @param {string} [options.defaultPath] The path at which browsing starts * @param {string} [options.title] An optional title that is shown at the top of the file browser. - * @param {boolean} [options.multiple] Whether or not to allow selecting multiple files (makes no sense with `openDirectory` enabled) - * @param {boolean} [options.filter] A file filter. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept + * @param {boolean} [options.multiple] Whether or not to allow selecting multiple files + * (makes no sense with `openDirectory` enabled) + * @param {boolean} [options.filter] A file filter. + * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept * @returns {Promise|string|false>}A promise that resolves * into a path to the selected file, or to an array of paths of files * (if options.multiple.) */ -window.showOpenDialog = function(options = {}) { +window.showOpenDialog = function showOpenDialog(options = {}) { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.style.opacity = 0; @@ -58,7 +60,7 @@ window.showOpenDialog = function(options = {}) { * @returns {Promise} A promise that resolves into a full path to a target file * if the user proceeded to save a file, and into `false` if the user cancelled the operation. */ -window.showSaveDialog = function (options = {}) { +window.showSaveDialog = function showSaveDialog(options = {}) { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.style.opacity = 0; diff --git a/src/js/codeEditorHelpers.js b/src/js/codeEditorHelpers.js index f23940c2a..d70f05cf1 100644 --- a/src/js/codeEditorHelpers.js +++ b/src/js/codeEditorHelpers.js @@ -1,316 +1,328 @@ -/* eslint-disable no-bitwise */ -/* eslint-disable no-underscore-dangle */ -(function() { - const {extend} = require('./data/node_requires/objectUtils'); - const fs = require('fs-extra'); - const path = require('path'); - /* global monaco riot */ - - const lib = [ - './data/typedefs/pixi.js.d.ts', - './data/typedefs/global.d.ts', - './data/typedefs/keywordWorkarounds.d.ts' - ]; - - window.signals = window.signals || riot.observable({}); - window.signals.on('monacoBooted', () => { - monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - noLib: true, - allowNonTsExtensions: true - }); - monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - noLib: true, - allowNonTsExtensions: true - }); - - for (const file of lib) { - fs.readFile(path.join(__dirname, file), { - encoding: 'utf-8' - }) - .then(data => { - monaco.languages.typescript.javascriptDefaults.addExtraLib(data); - monaco.languages.typescript.typescriptDefaults.addExtraLib(data); - }); - } - }); - - /** - * Adds custom hotkeys to the editors, specifically Ctrl+Plus, Ctrl+Minus for font size manipulation. - * @param {any} editor The editor to which to add hotkeys - * @returns {void} - */ - var extendHotkeys = (editor) => { - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_EQUAL, function() { - var num = Number(localStorage.fontSize); - if (num < 48) { - num++; - localStorage.fontSize = num; - window.signals.trigger('codeFontUpdated'); - } - return false; - }); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_MINUS, function() { - var num = Number(localStorage.fontSize); - if (num > 6) { - num--; - localStorage.fontSize = num; - window.signals.trigger('codeFontUpdated'); - } - return false; - }); - }; - - const isRangeSelection = function(s) { - if (s.selectionStartLineNumber !== s.positionLineNumber) { - return true; - } - if (s.selectionStartColumn !== s.positionColumn) { - return true; - } - return false; - }; - - // Tons of hacks go below, beware! - // But maybe it will heal through time - // @see https://github.com/microsoft/monaco-editor/issues/1661 and everything linked - const setUpWrappers = function(editor) { - editor.setPosition({ - column: 0, - lineNumber: 2 - }); - /* These signal to custom commands - that the current cursor's position is in the end/start of the editable range */ - const contextSOR = editor.createContextKey('startOfEditable', false), - contextEOR = editor.createContextKey('endOfEditable', false); - - const restrictSelections = function(selections) { - selections = selections || editor.getSelections(); - let resetSelections = false; - const model = editor.getModel(); - const maxLine = model.getLineCount() - 1; - const lastLineCol = model.getLineContent(Math.max(maxLine, 1)).length + 1; - - contextEOR.set(false); - contextSOR.set(false); - - for (const selection of selections) { - if (selection.selectionStartLineNumber < 2) { - selection.selectionStartLineNumber = 2; - selection.selectionStartColumn = 1; - resetSelections = true; - } - if (selection.positionLineNumber < 2) { - selection.positionLineNumber = 2; - selection.positionColumn = 1; - resetSelections = true; - } - if (selection.selectionStartLineNumber > maxLine) { - selection.selectionStartLineNumber = maxLine; - selection.selectionStartColumn = lastLineCol; - resetSelections = true; - } - if (selection.positionLineNumber > maxLine) { - selection.positionLineNumber = maxLine; - selection.positionColumn = lastLineCol; - resetSelections = true; - } - /* Get if any of the cursors happened to be in the beginning/end - of the editable range, so that we can block Delete/Backspace behavior. - Range selections are safe, as they delete the selected content, - not that is behind/in front of them. - */ - if (!isRangeSelection(selection)) { - if (selection.selectionStartLineNumber === 2 && - selection.selectionStartColumn === 1 - ) { - contextSOR.set(true); - } - if (selection.positionLineNumber === maxLine && - selection.positionColumn === lastLineCol - ) { - contextEOR.set(true); - } - } - } - if (resetSelections) { - editor.setSelections(selections); - } - }; - - // Turns out the Delete and Backspace keys do not produce a keyboard event but commands - // These commands overlay the default ones, thus cancelling the default behaviour - // @see https://github.com/microsoft/monaco-editor/issues/940 - const voidFunction = function() { - void 0; // magic! - }; - editor.addCommand(monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Delete, voidFunction, 'endOfEditable'); - editor.addCommand(monaco.KeyCode.Delete, voidFunction, 'endOfEditable'); - - // These cheat on the replace widget so that it cannot replace anything in the wrapper. - // Done by temporarily switching to "replace in selection" mode and back. - // Atomic replacements "work as expected" without this, for some reason or another. - const find = editor.getContribution('editor.contrib.findController'); - const oldReplaceAll = find.replaceAll.bind(find); - find.replaceAll = function () { - const oldSelections = editor.getSelections()? [...editor.getSelections()] : []; - const oldSearchScope = find._state.searchScope; - const oldGetFindScope = find._model._decorations.getFindScope; - if (!oldSearchScope) { - // Make up a new replacement scope - const model = editor.getModel(); - const maxLine = model.getLineCount() - 1; - const lastLineCol = model.getLineContent(Math.max(maxLine, 1)).length + 1; - const scope = { - endColumn: lastLineCol, - endLineNumber: maxLine, - positionColumn: 1, - positionLineNumber: 2, - selectionStartColumn: lastLineCol, - selectionStartLineNumber: maxLine, - startColumn: 1, - startLineNumber: 2 - }; - find._state.change({ - searchScope: {...scope} - }, true); - editor.setSelection(scope); - find._model._decorations.getFindScope = (function() { - return scope; - }); - } - - oldReplaceAll(); - - // Bring the previous editor state back - find._model._decorations.getFindScope = oldGetFindScope; - editor.setSelections(oldSelections); - find._state.change({ - searchScope: oldSearchScope - }, true); - find._widget._updateSearchScope(); - restrictSelections(); - }; - - // Clamp selections so they can't select wrapping lines - editor.onDidChangeCursorSelection(function(evt) { - const selections = [evt.selection, ...evt.secondarySelections]; - restrictSelections(selections); - }); - - const model = editor.getModel(); - const lastLine = model.getLineCount(); - editor.setHiddenAreas([{ - startLineNumber: 1, - endLineNumber: 1 - }, { - startLineNumber: lastLine, - endLineNumber: lastLine - }]); - }; - - const themeMappings = { - Day: 'tomorrow', - Night: 'ambiance', - Horizon: 'horizon', - default: 'tomorrow' - }; - const glob = require('./data/node_requires/glob'); - glob.codeEditorThemeMappings = themeMappings; - window.signals.on('UIThemeChanged', theme => { - monaco.editor.setTheme(themeMappings[theme]? themeMappings[theme] : themeMappings.default); - }); - window.signals.on('codeFontUpdated', () => { - const editorWrappers = document.querySelectorAll('.aCodeEditor'); - for (const editorWrap of editorWrappers) { - editorWrap.codeEditor.updateOptions({ - fontLigatures: localStorage.codeLigatures !== 'off', - lineHeight: (localStorage.codeDense === 'off'? 1.75 : 1.5) * Number(localStorage.fontSize), - fontSize: Number(localStorage.fontSize) - }); - } - }); - - var defaultOptions = { - language: 'plain_text', - fixedOverflowWidgets: true, - colorDecorators: true, - - scrollbar: { - verticalHasArrows: true, - horizontalHasArrows: true, - arrowSize: 24 - }, - - get fontFamily() { - return localStorage.fontFamily || 'Iosevka, monospace'; - }, - get theme() { - return themeMappings[localStorage.UItheme]? themeMappings[localStorage.UItheme] : themeMappings.default; - }, - get fontLigatures() { - return localStorage.codeLigatures !== 'off'; - }, - get lineHeight() { - return (localStorage.codeDense === 'off'? 1.75 : 1.5) * Number(localStorage.fontSize); - }, - get fontSize() { - return Number(localStorage.fontSize); - } - }; - - /** - * Mounts a Monaco editor on the passed tag. - * - * @global - * @param {HTMLTextareaElement|HTMLDivElement} tag A tag where an editor should be placed. It can be a textarea or any other block. - * @param {Object} [options] Options - * @param {String} [options.mode='plain_text'] Language mode. Sets syntacs highlighting and enables language checks, if relevant. Can be 'plain_text', 'markdown', 'javascript', 'html' or 'css' - * @returns {any} Editor instance - */ - window.setupCodeEditor = (tag, options) => { - const opts = extend(extend({}, defaultOptions), options); - opts.value = opts.value || tag.value || ''; - opts.value = opts.value.replace(/\r\n/g, '\n'); - if (opts.wrapper) { - opts.value = `${opts.wrapper[0]}\n${opts.value}\n${opts.wrapper[1]}`; - opts.lineNumbers = num => Math.max((num || 0) - 1, 1); - } - const codeEditor = monaco.editor.create(tag, opts); - tag.codeEditor = codeEditor; - codeEditor.tag = tag; - tag.classList.add(themeMappings[localStorage.UItheme]? themeMappings[localStorage.UItheme] : themeMappings.default); - - codeEditor.getModel()._options.defaultEOL = monaco.editor.DefaultEndOfLine.LF; - - codeEditor.getPureValue = function() { - const val = this.getValue(); - const start = opts.wrapper[0] + '\n', - end = '\n' + opts.wrapper[1]; - if (options.wrapper) { - if (val.indexOf(start) === 0 && - val.lastIndexOf(end) === (val.length - end.length) - ) { - return val.slice((start).length, -end.length); - } - } - return val; - }; - - if (opts.lockWrapper) { - setUpWrappers(codeEditor); - } - extendHotkeys(codeEditor); - - return codeEditor; - }; -})(this); +/* eslint-disable no-bitwise */ +/* eslint-disable no-underscore-dangle */ +(function codeEditorHelpers() { + const {extend} = require('./data/node_requires/objectUtils'); + const fs = require('fs-extra'); + const path = require('path'); + + const lib = [ + './data/typedefs/pixi.js.d.ts', + './data/typedefs/global.d.ts', + './data/typedefs/keywordWorkarounds.d.ts' + ]; + + window.signals = window.signals || riot.observable({}); + window.signals.on('monacoBooted', () => { + monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + noLib: true, + allowNonTsExtensions: true + }); + monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + noLib: true, + allowNonTsExtensions: true + }); + + for (const file of lib) { + fs.readFile(path.join(__dirname, file), { + encoding: 'utf-8' + }) + .then(ctTyping => { + monaco.languages.typescript.javascriptDefaults.addExtraLib(ctTyping); + monaco.languages.typescript.typescriptDefaults.addExtraLib(ctTyping); + }); + } + }); + + /** + * Adds custom hotkeys to the editors, specifically Ctrl+Plus, + * Ctrl+Minus for font size manipulation. + * @param {any} editor The editor to which to add hotkeys + * @returns {void} + */ + var extendHotkeys = (editor) => { + const zoomInCombo = monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_EQUAL; + editor.addCommand(zoomInCombo, function monacoZoomIn() { + var num = Number(localStorage.fontSize); + if (num < 48) { + num++; + localStorage.fontSize = num; + window.signals.trigger('codeFontUpdated'); + } + return false; + }); + const zoomOutCombo = monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_MINUS; + editor.addCommand(zoomOutCombo, function monacoZoomOut() { + var num = Number(localStorage.fontSize); + if (num > 6) { + num--; + localStorage.fontSize = num; + window.signals.trigger('codeFontUpdated'); + } + return false; + }); + }; + + const isRangeSelection = function (s) { + if (s.selectionStartLineNumber !== s.positionLineNumber) { + return true; + } + if (s.selectionStartColumn !== s.positionColumn) { + return true; + } + return false; + }; + + // Tons of hacks go below, beware! + // But maybe it will heal through time + // @see https://github.com/microsoft/monaco-editor/issues/1661 and everything linked + // eslint-disable-next-line max-lines-per-function + const setUpWrappers = function (editor) { + editor.setPosition({ + column: 0, + lineNumber: 2 + }); + /* These signal to custom commands + that the current cursor's position is in the end/start of the editable range */ + const contextSOR = editor.createContextKey('startOfEditable', false), + contextEOR = editor.createContextKey('endOfEditable', false); + + const restrictSelections = function (selections) { + selections = selections || editor.getSelections(); + let resetSelections = false; + const model = editor.getModel(); + const maxLine = model.getLineCount() - 1; + const lastLineCol = model.getLineContent(Math.max(maxLine, 1)).length + 1; + + contextEOR.set(false); + contextSOR.set(false); + + for (const selection of selections) { + if (selection.selectionStartLineNumber < 2) { + selection.selectionStartLineNumber = 2; + selection.selectionStartColumn = 1; + resetSelections = true; + } + if (selection.positionLineNumber < 2) { + selection.positionLineNumber = 2; + selection.positionColumn = 1; + resetSelections = true; + } + if (selection.selectionStartLineNumber > maxLine) { + selection.selectionStartLineNumber = maxLine; + selection.selectionStartColumn = lastLineCol; + resetSelections = true; + } + if (selection.positionLineNumber > maxLine) { + selection.positionLineNumber = maxLine; + selection.positionColumn = lastLineCol; + resetSelections = true; + } + /* Get if any of the cursors happened to be in the beginning/end + of the editable range, so that we can block Delete/Backspace behavior. + Range selections are safe, as they delete the selected content, + not that is behind/in front of them. */ + if (!isRangeSelection(selection)) { + if (selection.selectionStartLineNumber === 2 && + selection.selectionStartColumn === 1 + ) { + contextSOR.set(true); + } + if (selection.positionLineNumber === maxLine && + selection.positionColumn === lastLineCol + ) { + contextEOR.set(true); + } + } + } + if (resetSelections) { + editor.setSelections(selections); + } + }; + + // Turns out the Delete and Backspace keys do not produce a keyboard event but commands + // These commands overlay the default ones, thus cancelling the default behaviour + // @see https://github.com/microsoft/monaco-editor/issues/940 + const voidFunction = function () { + void 0; // magic! + }; + editor.addCommand(monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.Backspace, voidFunction, 'startOfEditable'); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Delete, voidFunction, 'endOfEditable'); + editor.addCommand(monaco.KeyCode.Delete, voidFunction, 'endOfEditable'); + + // These cheat on the replace widget so that it cannot replace anything in the wrapper. + // Done by temporarily switching to "replace in selection" mode and back. + // Atomic replacements "work as expected" without this, for some reason or another. + const find = editor.getContribution('editor.contrib.findController'); + const oldReplaceAll = find.replaceAll.bind(find); + find.replaceAll = function replaceAll() { + const oldSelections = editor.getSelections() ? [...editor.getSelections()] : []; + const oldSearchScope = find._state.searchScope; + const oldGetFindScope = find._model._decorations.getFindScope; + if (!oldSearchScope) { + // Make up a new replacement scope + const model = editor.getModel(); + const maxLine = model.getLineCount() - 1; + const lastLineCol = model.getLineContent(Math.max(maxLine, 1)).length + 1; + const scope = { + endColumn: lastLineCol, + endLineNumber: maxLine, + positionColumn: 1, + positionLineNumber: 2, + selectionStartColumn: lastLineCol, + selectionStartLineNumber: maxLine, + startColumn: 1, + startLineNumber: 2 + }; + find._state.change({ + searchScope: { + ...scope + } + }, true); + editor.setSelection(scope); + find._model._decorations.getFindScope = (function getFindScope() { + return scope; + }); + } + + oldReplaceAll(); + + // Bring the previous editor state back + find._model._decorations.getFindScope = oldGetFindScope; + editor.setSelections(oldSelections); + find._state.change({ + searchScope: oldSearchScope + }, true); + find._widget._updateSearchScope(); + restrictSelections(); + }; + + // Clamp selections so they can't select wrapping lines + editor.onDidChangeCursorSelection(function onChangeCursorSelection(evt) { + const selections = [evt.selection, ...evt.secondarySelections]; + restrictSelections(selections); + }); + + const model = editor.getModel(); + const lastLine = model.getLineCount(); + editor.setHiddenAreas([{ + startLineNumber: 1, + endLineNumber: 1 + }, { + startLineNumber: lastLine, + endLineNumber: lastLine + }]); + }; + + const themeMappings = { + Day: 'tomorrow', + Night: 'ambiance', + Horizon: 'horizon', + default: 'tomorrow' + }; + const glob = require('./data/node_requires/glob'); + glob.codeEditorThemeMappings = themeMappings; + window.signals.on('UIThemeChanged', theme => { + monaco.editor.setTheme(themeMappings[theme] ? themeMappings[theme] : themeMappings.default); + }); + window.signals.on('codeFontUpdated', () => { + const editorWrappers = document.querySelectorAll('.aCodeEditor'); + for (const editorWrap of editorWrappers) { + editorWrap.codeEditor.updateOptions({ + fontLigatures: localStorage.codeLigatures !== 'off', + lineHeight: (localStorage.codeDense === 'off' ? 1.75 : 1.5) * Number(localStorage.fontSize), + fontSize: Number(localStorage.fontSize) + }); + } + }); + + var defaultOptions = { + language: 'plain_text', + fixedOverflowWidgets: true, + colorDecorators: true, + + scrollbar: { + verticalHasArrows: true, + horizontalHasArrows: true, + arrowSize: 24 + }, + + get fontFamily() { + return localStorage.fontFamily || 'Iosevka, monospace'; + }, + get theme() { + return themeMappings[localStorage.UItheme] ? + themeMappings[localStorage.UItheme] : + themeMappings.default; + }, + get fontLigatures() { + return localStorage.codeLigatures !== 'off'; + }, + get lineHeight() { + return (localStorage.codeDense === 'off' ? 1.75 : 1.5) * Number(localStorage.fontSize); + }, + get fontSize() { + return Number(localStorage.fontSize); + } + }; + + /** + * Mounts a Monaco editor on the passed tag. + * + * @global + * @param {HTMLTextareaElement|HTMLDivElement} tag A tag where an editor should be placed. + * It can be a textarea or any other block. + * @param {Object} [options] Options + * @param {String} [options.mode='plain_text'] Language mode. Sets syntacs highlighting + * and enables language checks, if relevant. + * Can be 'plain_text', 'markdown', 'javascript', 'html' or 'css' + * @returns {any} Editor instance + */ + window.setupCodeEditor = (textarea, options) => { + const opts = extend(extend({}, defaultOptions), options); + opts.value = opts.value || textarea.value || ''; + opts.value = opts.value.replace(/\r\n/g, '\n'); + if (opts.wrapper) { + opts.value = `${opts.wrapper[0]}\n${opts.value}\n${opts.wrapper[1]}`; + opts.lineNumbers = num => Math.max((num || 0) - 1, 1); + } + const codeEditor = monaco.editor.create(textarea, opts); + textarea.codeEditor = codeEditor; + // eslint-disable-next-line id-blacklist + codeEditor.tag = textarea; + textarea.classList.add(themeMappings[localStorage.UItheme] ? + themeMappings[localStorage.UItheme] : + themeMappings.default); + + codeEditor.getModel()._options.defaultEOL = monaco.editor.DefaultEndOfLine.LF; + + codeEditor.getPureValue = function getPureValue() { + const val = this.getValue(); + const start = opts.wrapper[0] + '\n', + end = '\n' + opts.wrapper[1]; + if (options.wrapper) { + if (val.indexOf(start) === 0 && + val.lastIndexOf(end) === (val.length - end.length) + ) { + return val.slice((start).length, -end.length); + } + } + return val; + }; + + if (opts.lockWrapper) { + setUpWrappers(codeEditor); + } + extendHotkeys(codeEditor); + + return codeEditor; + }; +})(this); diff --git a/src/js/debuggerTools.js b/src/js/debuggerTools.js index c8e6ff728..df3b8b4fc 100644 --- a/src/js/debuggerTools.js +++ b/src/js/debuggerTools.js @@ -1,7 +1,7 @@ -(function () { +(function ctDebuggerTools() { var previewWindow; - window.openDebugger = function(link) { + window.openDebugger = function openDebugger(link) { if (previewWindow) { var nwWin = nw.Window.get(previewWindow); nwWin.show(); @@ -14,7 +14,7 @@ new_instance: false, id: 'ctPreview', title: 'ct.IDE Debugger' - }, function(newWin) { + }, function onDebuggerOpen(newWin) { var wind = newWin.window; previewWindow = wind; newWin.once('loaded', () => { diff --git a/src/js/firstRunSetup.js b/src/js/firstRunSetup.js index c8266d142..db33b3c25 100644 --- a/src/js/firstRunSetup.js +++ b/src/js/firstRunSetup.js @@ -1,14 +1,14 @@ -(function () {// first-launch setup - const defaults = { - fontSize: 18, - lastProjects: '', - notes: '', - appLanguage: 'English', - editorZooming: 0 - }; - for (const key in defaults) { - if (!(key in localStorage)) { - localStorage[key] = defaults[key]; - } - } -})(); +(function firstRunSetup() {// first-launch setup + const defaults = { + fontSize: 18, + lastProjects: '', + notes: '', + appLanguage: 'English', + editorZooming: 0 + }; + for (const key in defaults) { + if (!(key in localStorage)) { + localStorage[key] = defaults[key]; + } + } +})(); diff --git a/src/js/gulpWatch.js b/src/js/gulpWatch.js index 6691399ca..2792cfe24 100644 --- a/src/js/gulpWatch.js +++ b/src/js/gulpWatch.js @@ -1,13 +1,15 @@ -(function () { - try { - var reloading = false; - const gulp = require('gulp'); - const reload = () => { - if (!reloading) { - reloading = true; - nw.Window.get().reload(); - } - }; - gulp.watch(['./data/theme*.css', './index.html', './data/bundle.js', './data/js/**.js', './data/node_requires/**/*.js'], reload); - } catch (e) { void 0; } -})(); +(function gulpWatch() { + try { + var reloading = false; + const gulp = require('gulp'); + const reload = () => { + if (!reloading) { + reloading = true; + nw.Window.get().reload(); + } + }; + gulp.watch(['./data/theme*.css', './index.html', './data/bundle.js', './data/js/**.js', './data/node_requires/**/*.js'], reload); + } catch (e) { + void 0; + } +})(); diff --git a/src/js/links.js b/src/js/links.js index 791414dc1..ea54ec055 100644 --- a/src/js/links.js +++ b/src/js/links.js @@ -1,11 +1,11 @@ -(function () { - document.body.addEventListener('click', function(e) { - if (e.target && e.target.matches('a')) { - if (e.target.href) { - nw.Shell.openExternal(e.target.href); - e.stopPropagation(); - } - e.preventDefault(); - } - }); -})(); +(function catchExternalLinks() { + document.body.addEventListener('click', function externalLinkslistener(e) { + if (e.target && e.target.matches('a')) { + if (e.target.href) { + nw.Shell.openExternal(e.target.href); + e.stopPropagation(); + } + e.preventDefault(); + } + }); +})(); diff --git a/src/js/loadLanguageJSON.js b/src/js/loadLanguageJSON.js index 6915e729c..d1bce27a0 100644 --- a/src/js/loadLanguageJSON.js +++ b/src/js/loadLanguageJSON.js @@ -1,3 +1,3 @@ -(function () { - window.languageJSON = require('./data/node_requires/i18n.js').languageJSON; -})(); +(function loadLanguageJSON() { + window.languageJSON = require('./data/node_requires/i18n.js').languageJSON; +})(); diff --git a/src/js/loadProject.js b/src/js/loadProject.js index c2cd01daa..055c1e71a 100644 --- a/src/js/loadProject.js +++ b/src/js/loadProject.js @@ -1,6 +1,6 @@ -(function (window) { +(function addLoadProjectMethod(window) { window.migrationProcess = window.migrationProcess || []; - window.applyMigrationCode = function (version) { + window.applyMigrationCode = function applyMigrationCode(version) { const process = window.migrationProcess.find(process => process.version === version); if (!process) { throw new Error(`Cannot find migration code for version ${version}`); @@ -21,7 +21,7 @@ raw[3], // -next- versions and other postfixes will count as a fourth component. // They all will apply before regular versions - raw[4]? raw[5] || 1 : null + raw[4] ? raw[5] || 1 : null ]; }; @@ -110,7 +110,7 @@ fs.ensureDir(global.projdir + '/img'); fs.ensureDir(global.projdir + '/snd'); - const lastProjects = localStorage.lastProjects? localStorage.lastProjects.split(';') : []; + const lastProjects = localStorage.lastProjects ? localStorage.lastProjects.split(';') : []; if (lastProjects.indexOf(path.normalize(global.projdir + '.ict')) !== -1) { lastProjects.splice(lastProjects.indexOf(path.normalize(global.projdir + '.ict')), 1); } @@ -141,22 +141,22 @@ * @returns {void} */ var loadProjectFile = async proj => { - const data = await fs.readFile(proj, 'utf8'); + const textProjData = await fs.readFile(proj, 'utf8'); let projectData; // Before v1.3, projects were stored in JSON format try { - if (data.indexOf('{') === 0) { // First, make a silly check for JSON files - projectData = JSON.parse(data); + if (textProjData.indexOf('{') === 0) { // First, make a silly check for JSON files + projectData = JSON.parse(textProjData); } else { try { const YAML = require('js-yaml'); - projectData = YAML.safeLoad(data); + projectData = YAML.safeLoad(textProjData); } catch (e) { // whoopsie, wrong window // eslint-disable-next-line no-console console.warn(`Tried to load a file ${proj} as a YAML, but got an error (see below). Falling back to JSON.`); console.error(e); - projectData = JSON.parse(data); + projectData = JSON.parse(textProjData); } } } catch (e) { @@ -193,10 +193,9 @@ */ .confirm(voc.message .replace('{0}', targetStat.mtime.toLocaleString()) - .replace('{1}', targetStat.mtime < stat.mtime? voc.older : voc.newer) + .replace('{1}', targetStat.mtime < stat.mtime ? voc.older : voc.newer) .replace('{2}', stat.mtime.toLocaleString()) - .replace('{3}', stat.mtime < targetStat.mtime? voc.older : voc.newer) - ) + .replace('{3}', stat.mtime < targetStat.mtime ? voc.older : voc.newer)) .then(e => { if (e.buttonClicked === 'ok') { loadProjectFile(proj + '.recovery'); diff --git a/src/js/migration/0.3.0.js b/src/js/migration/0.3.0.js index d3d2b790c..066aeb992 100644 --- a/src/js/migration/0.3.0.js +++ b/src/js/migration/0.3.0.js @@ -10,7 +10,7 @@ window.migrationProcess.push({ /* replace numerical IDs with RFC4122 version 4 UIDs */ let startingRoom; const graphmap = {}, - typemap = {}; + typemap = {}; for (const graph of project.graphs) { graph.uid = generateGUID(); graphmap[graph.origname] = graph.uid; diff --git a/src/js/migration/1.3.0.js b/src/js/migration/1.3.0.js index 83f62c5a4..67c20685a 100644 --- a/src/js/migration/1.3.0.js +++ b/src/js/migration/1.3.0.js @@ -11,13 +11,14 @@ window.migrationProcess.push({ }; for (const texture of project.textures) { if (!('padding' in texture)) { - texture.padding = texture.tiled? 0 : 1; + texture.padding = texture.tiled ? 0 : 1; } } + const oldSettings = project.settings.export; project.settings.export = { - linux: project.settings.export.linux || project.settings.export.linux64 || project.settings.export.linux32, - windows: project.settings.export.windows || project.settings.export.windows64 || project.settings.export.windows32, - mac: project.settings.export.mac || project.settings.export.mac64 + linux: oldSettings.linux || oldSettings.linux64 || oldSettings.linux32, + windows: oldSettings.windows || oldSettings.windows64 || oldSettings.windows32, + mac: oldSettings.mac || oldSettings.mac64 }; project.settings.desktopMode = project.settings.desktopMode || 'maximized'; resolve(); diff --git a/src/js/riotMixins.js b/src/js/riotMixins.js index 177e07d54..3cdf785b4 100644 --- a/src/js/riotMixins.js +++ b/src/js/riotMixins.js @@ -1,89 +1,89 @@ -(function () { - const glob = require('./data/node_requires/glob'); - var wire = (that, field, update) => e => { - var way = field.split(/(? 1) { - root = root[way[0]]; - way.shift(); - } - if (e.target.type === 'checkbox') { - val = e.target.checked; - } else if (e.target.type === 'number' || e.target.type === 'range') { - val = Number(e.target.value); - if (e.target.hasAttribute('data-wired-force-minmax')) { - val = Math.max(Number(e.target.min), Math.min(Number(e.target.max), val)); - } - } else { - val = e.target.value; - } - root[way[0]] = val; - glob.modified = true; - if (update && ('update' in that)) { - that.update(); - } - return val; - }; - window.riotWired = { - init() { - this.wire = wire.bind(this, this); - } - }; - - var voc = tag => { - const updateLocales = () => { - if (tag.namespace) { - const way = tag.namespace.split(/(? { - window.signals.off('updateLocales', updateLocales); - }); - }; - window.riotVoc = { - init() { - voc(this); - } - }; - - var niceTime = function(date) { - if (!(date instanceof Date)) { - date = new Date(date); - } - const today = new Date(); - if (date.getDate() !== today.getDate() || - date.getFullYear() !== today.getFullYear() || - date.getMonth() !== today.getMonth() - ) { - return date.toLocaleDateString(); - } - return date.toLocaleTimeString(); - }; - window.riotNiceTime = { - init() { - this.niceTime = niceTime; - } - }; -})(); +(function riotMixins() { + const glob = require('./data/node_requires/glob'); + var wire = (that, field, update) => e => { + var way = field.split(/(? 1) { + root = root[way[0]]; + way.shift(); + } + if (e.target.type === 'checkbox') { + val = e.target.checked; + } else if (e.target.type === 'number' || e.target.type === 'range') { + val = Number(e.target.value); + if (e.target.hasAttribute('data-wired-force-minmax')) { + val = Math.max(Number(e.target.min), Math.min(Number(e.target.max), val)); + } + } else { + val = e.target.value; + } + root[way[0]] = val; + glob.modified = true; + if (update && ('update' in that)) { + that.update(); + } + return val; + }; + window.riotWired = { + init() { + this.wire = wire.bind(this, this); + } + }; + + var voc = riotTag => { + const updateLocales = () => { + if (riotTag.namespace) { + const way = riotTag.namespace.split(/(? { + window.signals.off('updateLocales', updateLocales); + }); + }; + window.riotVoc = { + init() { + voc(this); + } + }; + + var niceTime = function (date) { + if (!(date instanceof Date)) { + date = new Date(date); + } + const today = new Date(); + if (date.getDate() !== today.getDate() || + date.getFullYear() !== today.getFullYear() || + date.getMonth() !== today.getMonth() + ) { + return date.toLocaleDateString(); + } + return date.toLocaleTimeString(); + }; + window.riotNiceTime = { + init() { + this.niceTime = niceTime; + } + }; +})(); diff --git a/src/js/roomCopyTools.js b/src/js/roomCopyTools.js index eef588a3a..b4fc19cd5 100644 --- a/src/js/roomCopyTools.js +++ b/src/js/roomCopyTools.js @@ -1,4 +1,4 @@ -(function () { +(function roomCopyTools() { const clickThreshold = 16; const glob = require('./data/node_requires/glob'); @@ -27,18 +27,21 @@ this.refs.canvas.x.drawImage( img, ox, oy, w, h, - e.offsetX / this.zoomFactor - grax, e.offsetY / this.zoomFactor - gray, w, h); + e.offsetX / this.zoomFactor - grax, e.offsetY / this.zoomFactor - gray, w, h + ); } else { // если есть сетка, то координаты предварительной копии нужно отснэпить по сетке var dx = this.xToRoom(e.offsetX), dy = this.yToRoom(e.offsetY); w = texture.width; h = texture.height; + const {room} = this; this.refs.canvas.x.drawImage( img, ox, oy, w, h, - this.xToCanvas(Math.round(dx / this.room.gridX) * this.room.gridX) / this.zoomFactor - grax, - this.yToCanvas(Math.round(dy / this.room.gridY) * this.room.gridY) / this.zoomFactor - gray, - w, h); + this.xToCanvas(Math.round(dx / room.gridX) * room.gridX) / this.zoomFactor - grax, + this.yToCanvas(Math.round(dy / room.gridY) * room.gridY) / this.zoomFactor - gray, + w, h + ); } }; const selectACopyAt = function (e) { @@ -50,7 +53,7 @@ const layerCopies = this.room.copies; for (let j = 0, lj = layerCopies.length; j < lj; j++) { const xp = layerCopies[j].x - fromx, - yp = layerCopies[j].y - fromy; + yp = layerCopies[j].y - fromy; l = Math.sqrt(xp * xp + yp * yp); if (l < length) { length = l; @@ -60,28 +63,32 @@ return this.room.copies[pos]; }; - window.roomCopyTools = { - init() { - this.currentType = -1; - this.onCanvasPressCopies = e => { - if (this.selectedCopies && !e.shiftKey && e.button === 0) { + const onCanvasPressCopies = function (e) { + if (this.selectedCopies && !e.shiftKey && e.button === 0) { + for (const copy of this.selectedCopies) { + var x = this.xToRoom(this.startx), + y = this.yToRoom(this.starty); + const textureId = global.currentProject.types[glob.typemap[copy.uid]].texture; + const {g} = glob.texturemap[textureId]; + if (x > copy.x - g.axis[0] && y > copy.y - g.axis[1] && + x < copy.x - g.axis[0] + g.width && y < copy.y - g.axis[1] + g.height) { + this.movingStuff = true; for (const copy of this.selectedCopies) { - var x = this.xToRoom(this.startx), - y = this.yToRoom(this.starty); - const {g} = glob.texturemap[global.currentProject.types[glob.typemap[copy.uid]].texture]; - if (x > copy.x - g.axis[0] && y > copy.y - g.axis[1] && - x < copy.x - g.axis[0] + g.width && y < copy.y - g.axis[1] + g.height) { - this.movingStuff = true; - for (const copy of this.selectedCopies) { - copy.lastX = copy.x; - copy.lastY = copy.y; - } - return true; - } + copy.lastX = copy.x; + copy.lastY = copy.y; } + return true; } - return false; - }; + } + } + return false; + }; + + window.roomCopyTools = { + // eslint-disable-next-line max-lines-per-function + init() { + this.currentType = -1; + this.onCanvasPressCopies = onCanvasPressCopies; const selectCopies = e => { var x1 = this.xToRoom(this.startx), y1 = this.yToRoom(this.starty), @@ -92,7 +99,8 @@ ymin = Math.min(y1, y2), ymax = Math.max(y1, y2); for (const copy of this.room.copies) { - const {g} = glob.texturemap[global.currentProject.types[glob.typemap[copy.uid]].texture]; + const textureId = global.currentProject.types[glob.typemap[copy.uid]].texture; + const {g} = glob.texturemap[textureId]; const x1 = copy.x - g.axis[0] * (copy.tx || 1), x2 = copy.x - (g.axis[0] - g.width) * (copy.tx || 1), y1 = copy.y - g.axis[1] * (copy.ty || 1), @@ -112,7 +120,8 @@ }; this.onCanvasMouseUpCopies = e => { if (e.button === 0 && this.currentType === -1 && e.shiftKey) { - if (Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold) { + const dragLength = Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty); + if (dragLength > clickThreshold) { // Было прямоугольное выделение if (!this.selectedCopies) { this.selectedCopies = []; @@ -123,7 +132,9 @@ } } else { // Был единичный выбор - if (!this.room.copies.length) { return; } + if (!this.room.copies.length) { + return; + } const copy = selectACopyAt.apply(this, [e]); if (this.selectedCopies) { const ind = this.selectedCopies.indexOf(copy); @@ -145,21 +156,23 @@ } this.refreshRoomCanvas(); }; - // При клике на канвас помещает копию на соответствующий слой + // Place a copy on click this.onCanvasClickCopies = e => { - if ( - Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold && + if (Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold && !e.shiftKey ) { return; // this looks neither like a regular click nor like a Shift+drag } - // Отмена выделения копий, если таковые были, при клике - if (this.selectedCopies && !this.movingStuff && !(e.shiftKey && this.currentType === -1)) { + // Cancel copy selection on click + if (this.selectedCopies && + !this.movingStuff && + !(e.shiftKey && this.currentType === -1) + ) { this.selectedCopies = false; this.refreshRoomCanvas(); return; } - // Если не выбран тип создаваемой копии, или идёт удаление копий, то ничего не делаем + // If no type was picked or we delete stuff, do nothing if ((this.currentType === -1 || e.ctrlKey) && e.button === 0) { return; } @@ -216,7 +229,8 @@ } this.drawDeleteCircle(e); } else if (this.mouseDown && e.shiftKey) { - if (Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold) { + const dragLength = Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty); + if (dragLength > clickThreshold) { this.refreshRoomCanvas(e); // рисовка прямоугольного выделения const x1 = this.xToRoom(this.startx), @@ -243,7 +257,11 @@ this.refreshRoomCanvas(e); } else if (this.currentType !== -1) { drawInsertPreview.apply(this, [e]); - } else if (this.mouseDown && e.shift && Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold) { + } else if ( + this.mouseDown && + e.shift && + Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold + ) { this.refreshRoomCanvas(e); // рисовка прямоугольного выделения const x1 = this.xToRoom(this.startx), @@ -253,9 +271,12 @@ this.drawSelection(x1, y1, x2, y2); } }; + // eslint-disable-next-line id-length this.onCanvasContextMenuCopies = e => { - // Сначала ищется ближайшая к курсору копия. Если слоёв в комнате нет, то всё отменяется - if (!this.room.copies.length) { return; } + // Find the closest copy. If there are no copies, abort + if (!this.room.copies.length) { + return; + } var copy = selectACopyAt.apply(this, [e]), type = global.currentProject.types[glob.typemap[copy.uid]]; this.closestType = type; @@ -312,6 +333,7 @@ } }] }; + // eslint-disable-next-line id-length this.onCanvasContextMenuMultipleCopies = e => { this.forbidDrawing = true; setTimeout(() => { diff --git a/src/js/roomTileTools.js b/src/js/roomTileTools.js index 6819d823b..8ebd72aef 100644 --- a/src/js/roomTileTools.js +++ b/src/js/roomTileTools.js @@ -1,149 +1,164 @@ -(function () { +(function roomTileTools() { const clickThreshold = 16; const glob = require('./data/node_requires/glob'); - window.roomTileTools = { - init() { - this.onCanvasMouseUpTiles = e => { - if (e.button === 0 && this.currentTileLayer && Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold) { - // Было прямоугольное выделение - this.selectedTiles = []; - var x1 = this.xToRoom(this.startx), - y1 = this.yToRoom(this.starty), - x2 = this.xToRoom(e.offsetX), - y2 = this.yToRoom(e.offsetY), - xmin = Math.min(x1, x2), - xmax = Math.max(x1, x2), - ymin = Math.min(y1, y2), - ymax = Math.max(y1, y2); - for (const tile of this.currentTileLayer.tiles) { - const {g} = glob.texturemap[tile.texture]; - if (tile.x > xmin && tile.x + g.width < xmax && - tile.y > ymin && tile.y + g.height < ymax) { - this.selectedTiles.push(tile); - } - } - this.refreshRoomCanvas(); + + const onCanvasMouseUpTiles = function (e) { + if (e.button === 0 && + this.currentTileLayer && + Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold + ) { + // Было прямоугольное выделение + this.selectedTiles = []; + var x1 = this.xToRoom(this.startx), + y1 = this.yToRoom(this.starty), + x2 = this.xToRoom(e.offsetX), + y2 = this.yToRoom(e.offsetY), + xmin = Math.min(x1, x2), + xmax = Math.max(x1, x2), + ymin = Math.min(y1, y2), + ymax = Math.max(y1, y2); + for (const tile of this.currentTileLayer.tiles) { + const {g} = glob.texturemap[tile.texture]; + if (tile.x > xmin && tile.x + g.width < xmax && + tile.y > ymin && tile.y + g.height < ymax) { + this.selectedTiles.push(tile); } - }; - this.onCanvasMoveTiles = e => { - // if we delete tiles - if (e.ctrlKey) { - // and the mouse is held down - if (this.mouseDown && this.currentTileLayer) { - var pos = 0, - l, - done = false, - fromx = this.xToRoom(e.offsetX), - fromy = this.yToRoom(e.offsetY); - var maxdist = Math.max(this.room.gridX, this.room.gridY); - for (let j = 0, lj = this.currentTileLayer.tiles.length; j < lj; j++) { - const xp = this.currentTileLayer.tiles[j].x - fromx, - yp = this.currentTileLayer.tiles[j].y - fromy; - l = Math.sqrt(xp * xp + yp * yp); - if (l < maxdist) { - pos = j; - done = true; - break; - } - } - if (done) { - this.currentTileLayer.tiles.splice(pos, 1); - } - } - this.drawDeleteCircle(e); - } else if (this.mouseDown && Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold) { - this.refreshRoomCanvas(e); - // draw the rectangular selection frame - const x1 = this.xToRoom(this.startx), - x2 = this.xToRoom(e.offsetX), - y1 = this.yToRoom(this.starty), - y2 = this.yToRoom(e.offsetY); - this.drawSelection(x1, y1, x2, y2); - } else if (this.currentTileset) { - // preview of tile placement - this.refreshRoomCanvas(e); - this.refs.canvas.x.setTransform(this.zoomFactor, 0, 0, this.zoomFactor, 0, 0); - this.refs.canvas.x.globalAlpha = 0.5; - const img = glob.texturemap[this.currentTileset.uid], - texture = this.currentTileset; - const sx = texture.offx + (texture.width + texture.marginx) * this.tileX - texture.marginx, - sy = texture.offy + (texture.height + texture.marginy) * this.tileY - texture.marginy, - w = (texture.width + texture.marginx) * this.tileSpanX - texture.marginx, - h = (texture.height + texture.marginy) * this.tileSpanY - texture.marginy; - if (this.room.gridX === 0 || e.altKey) { - this.refs.canvas.x.drawImage( - img, - sx, sy, w, h, - e.offsetX / this.zoomFactor, - e.offsetY / this.zoomFactor, - w, h); - } else { - // snap coordinates to a grid if it is enabled - const dx = this.xToRoom(e.offsetX), - dy = this.yToRoom(e.offsetY); - this.refs.canvas.x.drawImage( - img, - sx, sy, w, h, - this.xToCanvas(Math.round(dx / this.room.gridX) * this.room.gridX) / this.zoomFactor, - this.yToCanvas(Math.round(dy / this.room.gridY) * this.room.gridY) / this.zoomFactor, - w, h); + } + this.refreshRoomCanvas(); + } + }; + const onCanvasMoveTiles = function (e) { + // if we delete tiles + if (e.ctrlKey) { + // and the mouse is held down + if (this.mouseDown && this.currentTileLayer) { + var pos = 0, + l, + done = false, + fromx = this.xToRoom(e.offsetX), + fromy = this.yToRoom(e.offsetY); + var maxdist = Math.max(this.room.gridX, this.room.gridY); + for (let j = 0, lj = this.currentTileLayer.tiles.length; j < lj; j++) { + const xp = this.currentTileLayer.tiles[j].x - fromx, + yp = this.currentTileLayer.tiles[j].y - fromy; + l = Math.sqrt(xp * xp + yp * yp); + if (l < maxdist) { + pos = j; + done = true; + break; } } - }; - - this.onCanvasClickTiles = e => { - if ( - Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold && - !e.shiftKey - ) { - return; // this looks neither like a regular click nor like a Shift+drag + if (done) { + this.currentTileLayer.tiles.splice(pos, 1); } + } + this.drawDeleteCircle(e); + } else if ( + this.mouseDown && + Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold + ) { + this.refreshRoomCanvas(e); + // draw the rectangular selection frame + const x1 = this.xToRoom(this.startx), + x2 = this.xToRoom(e.offsetX), + y1 = this.yToRoom(this.starty), + y2 = this.yToRoom(e.offsetY); + this.drawSelection(x1, y1, x2, y2); + } else if (this.currentTileset) { + // preview of tile placement + this.refreshRoomCanvas(e); + this.refs.canvas.x.setTransform(this.zoomFactor, 0, 0, this.zoomFactor, 0, 0); + this.refs.canvas.x.globalAlpha = 0.5; + const img = glob.texturemap[this.currentTileset.uid], + tex = this.currentTileset; + const sx = tex.offx + (tex.width + tex.marginx) * this.tileX - tex.marginx, + sy = tex.offy + (tex.height + tex.marginy) * this.tileY - tex.marginy, + w = (tex.width + tex.marginx) * this.tileSpanX - tex.marginx, + h = (tex.height + tex.marginy) * this.tileSpanY - tex.marginy; + const {room} = this; + if (room.gridX === 0 || e.altKey) { + this.refs.canvas.x.drawImage( + img, + sx, sy, w, h, + e.offsetX / this.zoomFactor, + e.offsetY / this.zoomFactor, + w, h + ); + } else { + // snap coordinates to a grid if it is enabled + const dx = this.xToRoom(e.offsetX), + dy = this.yToRoom(e.offsetY); + this.refs.canvas.x.drawImage( + img, + sx, sy, w, h, + this.xToCanvas(Math.round(dx / room.gridX) * room.gridX) / this.zoomFactor, + this.yToCanvas(Math.round(dy / room.gridY) * room.gridY) / this.zoomFactor, + w, h + ); + } + } + }; - // cancel potential tile selection on click - if ( - this.selectedTiles && !this.movingStuff && - !(e.shiftKey && !this.currentTileset) - ) { - this.selectedTiles = false; - this.refreshRoomCanvas(); - return; - } - // no selection was there, and the tileset is not selected, or user deletes tiles - if ((!this.currentTileset || e.ctrlKey) && e.button === 0) { - return; - } - // insert tiles - if (Number(this.room.gridX) === 0 || e.altKey) { - if (this.lastTileX !== Math.floor(this.xToRoom(e.offsetX)) || - this.lastTileY !== Math.floor(this.yToRoom(e.offsetY)) - ) { - this.lastTileX = Math.floor(this.xToRoom(e.offsetX)); - this.lastTileY = Math.floor(this.yToRoom(e.offsetY)); - this.currentTileLayer.tiles.push({ - x: this.lastTileX, - y: this.lastTileY, - texture: this.currentTileset.uid, - grid: [this.tileX, this.tileY, this.tileSpanX, this.tileSpanY] - }); - } - } else { - var x = Math.floor(this.xToRoom(e.offsetX)), - y = Math.floor(this.yToRoom(e.offsetY)); - if (this.lastTileX !== Math.round(x / this.room.gridX) * this.room.gridX || - this.lastTileY !== Math.round(y / this.room.gridY) * this.room.gridY - ) { - this.lastTileX = Math.round(x / this.room.gridX) * this.room.gridX; - this.lastTileY = Math.round(y / this.room.gridY) * this.room.gridY; - this.currentTileLayer.tiles.push({ - x: this.lastTileX, - y: this.lastTileY, - texture: this.currentTileset.uid, - grid: [this.tileX, this.tileY, this.tileSpanX, this.tileSpanY] - }); - } - } - this.refreshRoomCanvas(); - }; + const onCanvasClickTiles = function (e) { + if ( + Math.hypot(e.offsetX - this.startx, e.offsetY - this.starty) > clickThreshold && + !e.shiftKey + ) { + return; // this looks neither like a regular click nor like a Shift+drag + } + + // cancel potential tile selection on click + if ( + this.selectedTiles && !this.movingStuff && + !(e.shiftKey && !this.currentTileset) + ) { + this.selectedTiles = false; + this.refreshRoomCanvas(); + return; + } + // no selection was there, and the tileset is not selected, or user deletes tiles + if ((!this.currentTileset || e.ctrlKey) && e.button === 0) { + return; + } + // insert tiles + if (Number(this.room.gridX) === 0 || e.altKey) { + if (this.lastTileX !== Math.floor(this.xToRoom(e.offsetX)) || + this.lastTileY !== Math.floor(this.yToRoom(e.offsetY)) + ) { + this.lastTileX = Math.floor(this.xToRoom(e.offsetX)); + this.lastTileY = Math.floor(this.yToRoom(e.offsetY)); + this.currentTileLayer.tiles.push({ + x: this.lastTileX, + y: this.lastTileY, + texture: this.currentTileset.uid, + grid: [this.tileX, this.tileY, this.tileSpanX, this.tileSpanY] + }); + } + } else { + var x = Math.floor(this.xToRoom(e.offsetX)), + y = Math.floor(this.yToRoom(e.offsetY)); + if (this.lastTileX !== Math.round(x / this.room.gridX) * this.room.gridX || + this.lastTileY !== Math.round(y / this.room.gridY) * this.room.gridY + ) { + this.lastTileX = Math.round(x / this.room.gridX) * this.room.gridX; + this.lastTileY = Math.round(y / this.room.gridY) * this.room.gridY; + this.currentTileLayer.tiles.push({ + x: this.lastTileX, + y: this.lastTileY, + texture: this.currentTileset.uid, + grid: [this.tileX, this.tileY, this.tileSpanX, this.tileSpanY] + }); + } + } + this.refreshRoomCanvas(); + }; + + window.roomTileTools = { + // eslint-disable-next-line max-lines-per-function + init() { + this.onCanvasMouseUpTiles = onCanvasMouseUpTiles; + this.onCanvasMoveTiles = onCanvasMoveTiles; + this.onCanvasClickTiles = onCanvasClickTiles; // context menu while clicking on the canvas w/out selection this.roomCanvasTileMenu = { @@ -159,7 +174,9 @@ }; this.onCanvasContextMenuTiles = e => { // Search for the closest tile. If no tiles found, exit. - if (!this.room.tiles.length || !this.currentTileLayer.tiles.length) {return false;} + if (!this.room.tiles.length || !this.currentTileLayer.tiles.length) { + return false; + } var pos = 0, length = Infinity, l, @@ -167,7 +184,7 @@ fromy = this.yToRoom(e.offsetY); for (let i = 0, li = this.currentTileLayer.tiles.length; i < li; i++) { const xp = this.currentTileLayer.tiles[i].x - fromx, - yp = this.currentTileLayer.tiles[i].y - fromy; + yp = this.currentTileLayer.tiles[i].y - fromy; l = Math.sqrt(xp * xp + yp * yp); if (l < length) { length = l; @@ -175,15 +192,15 @@ } } var tile = this.currentTileLayer.tiles[pos], - texture = glob.texturemap[tile.texture].g; + tex = glob.texturemap[tile.texture].g; this.closestPos = pos; // draw the tile preview this.refreshRoomCanvas(); var left = tile.x - 1.5, top = tile.y - 1.5, - width = ((texture.width + texture.marginx) * tile.grid[2]) - texture.marginx + 3, - height = ((texture.height + texture.marginy) * tile.grid[3]) - texture.marginy + 3; - this.drawSelection(left, top, left+width, top+height); + width = ((tex.width + tex.marginx) * tile.grid[2]) - tex.marginx + 3, + height = ((tex.height + tex.marginy) * tile.grid[3]) - tex.marginy + 3; + this.drawSelection(left, top, left + width, top + height); this.forbidDrawing = true; setTimeout(() => { @@ -201,7 +218,8 @@ label: window.languageJSON.roomview.deletetiles, click: () => { for (const tile of this.selectedTiles) { - this.currentTileLayer.tiles.splice(this.currentTileLayer.tiles.indexOf(tile), 1); + const ind = this.currentTileLayer.tiles.indexOf(tile); + this.currentTileLayer.tiles.splice(ind, 1); } this.selectedTiles = false; this.refreshRoomCanvas(); @@ -229,7 +247,8 @@ this.room.tiles.push(layer); } for (const tile of this.selectedTiles) { - this.currentTileLayer.tiles.splice(this.currentTileLayer.tiles.indexOf(tile), 1); + const ind = this.currentTileLayer.tiles.indexOf(tile); + this.currentTileLayer.tiles.splice(ind, 1); layer.tiles.push(tile); } this.selectedTiles = false; @@ -263,6 +282,7 @@ } }] }; + // eslint-disable-next-line id-length this.onCanvasContextMenuMultipleTiles = e => { this.forbidDrawing = true; setTimeout(() => { diff --git a/src/js/userScriptsNotice.js b/src/js/userScriptsNotice.js index cfe7e47af..2580ea4d3 100644 --- a/src/js/userScriptsNotice.js +++ b/src/js/userScriptsNotice.js @@ -1,4 +1,4 @@ -(function () { +(function userScriptsNotice() { try { // Is it a ct.js in a dev folder? require('gulp'); diff --git a/src/js/windowWatcher.js b/src/js/windowWatcher.js index 5d7422262..6ab9f356f 100644 --- a/src/js/windowWatcher.js +++ b/src/js/windowWatcher.js @@ -1,4 +1,4 @@ -(function () { +(function windowWatcher() { if (!document.body.hasAttribute('data-manage-window')) { return; } @@ -13,7 +13,7 @@ lastState = 'maximized'; } localStorage.windowSettings = JSON.stringify({ - mode: win.isFullscreen? 'fullscreen' : lastState + mode: win.isFullscreen ? 'fullscreen' : lastState }); }; win.on('restore', () => { @@ -23,21 +23,23 @@ win.on('maximize', () => { maximized = true; }); - win.on('move', function () { + win.on('move', function windowMoveListener() { maximized = false; saveState(); }); - window.addEventListener('resize', function () { + window.addEventListener('resize', function windowResizeListener() { maximized = false; saveState(); }); - win.on('closed', function () { + win.on('closed', function windowCloseListener() { saveState(); }); - const settings = localStorage.windowSettings? JSON.parse(localStorage.windowSettings) : { - mode: 'center' - }; + const settings = localStorage.windowSettings ? + JSON.parse(localStorage.windowSettings) : + { + mode: 'center' + }; if (settings.mode === 'fullscreen') { win.enterFullscreen(); } else if (settings.mode === 'maximized') { diff --git a/src/js/zoomSetup.js b/src/js/zoomSetup.js index 06fa43b8b..d4965a1fc 100644 --- a/src/js/zoomSetup.js +++ b/src/js/zoomSetup.js @@ -1,5 +1,5 @@ -// Set the correct zoom -(function () { +(function zoomSetup() { + // Set the correct zoom const win = nw.Window.get(); if (win.zoomLevel !== Number(localStorage.editorZooming || 0)) { win.zoomLevel = Number(localStorage.editorZooming || 0); diff --git a/src/node_requires/exporter/css.js b/src/node_requires/exporter/css.js index a65ccdb43..096c7fb0c 100644 --- a/src/node_requires/exporter/css.js +++ b/src/node_requires/exporter/css.js @@ -2,15 +2,15 @@ const substituteCssVars = (str, project, injects) => { const Color = global.brehautColor; let color1 = project.settings.branding.accent, // eslint-disable-next-line new-cap - color2 = (Color(project.settings.branding.accent).getLuminance() < 0.5)? '#ffffff' : '#000000'; + color2 = (Color(project.settings.branding.accent).getLuminance() < 0.5) ? '#ffffff' : '#000000'; if (project.settings.branding.invertPreloaderScheme) { [color1, color2] = [color2, color1]; } return str - .replace('/*@pixelatedrender@*/', project.settings.pixelatedrender? 'canvas,img{image-rendering:optimizeSpeed;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:optimize-contrast;image-rendering:pixelated;ms-interpolation-mode:nearest-neighbor}' : '') - .replace(/\/\*@preloaderforeground@\*\//g, color1) - .replace(/\/\*@preloaderbackground@\*\//g, color2) - .replace('/*%css%*/', injects.css); + .replace('/*@pixelatedrender@*/', project.settings.pixelatedrender ? 'canvas,img{image-rendering:optimizeSpeed;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:optimize-contrast;image-rendering:pixelated;ms-interpolation-mode:nearest-neighbor}' : '') + .replace(/\/\*@preloaderforeground@\*\//g, color1) + .replace(/\/\*@preloaderbackground@\*\//g, color2) + .replace('/*%css%*/', injects.css); }; module.exports = { substituteCssVars diff --git a/src/node_requires/exporter/emitterTandems.js b/src/node_requires/exporter/emitterTandems.js index f4b3a907e..e55717504 100644 --- a/src/node_requires/exporter/emitterTandems.js +++ b/src/node_requires/exporter/emitterTandems.js @@ -15,4 +15,6 @@ const stringifyTandems = project => { return JSON.stringify(tandems, null, ' '); }; -module.exports = {stringifyTandems}; +module.exports = { + stringifyTandems +}; diff --git a/src/node_requires/exporter/fonts.js b/src/node_requires/exporter/fonts.js index 993027874..a6e491256 100644 --- a/src/node_requires/exporter/fonts.js +++ b/src/node_requires/exporter/fonts.js @@ -6,9 +6,9 @@ const stringifyFont = font => ` src: url('fonts/${font.origname}.woff') format('woff'), url('fonts/${font.origname}') format('truetype'); font-weight: ${font.weight}; - font-style: ${font.italic? 'italic' : 'normal'}; + font-style: ${font.italic ? 'italic' : 'normal'}; }`; -const bundleFonts = async function(proj, projdir, writeDir) { +const bundleFonts = async function (proj, projdir, writeDir) { let css = '', js = ''; const writePromises = []; @@ -23,10 +23,10 @@ const bundleFonts = async function(proj, projdir, writeDir) { try { woff = new Buffer(ttf2woff(ttf).buffer); } catch (e) { - window.alertify.error(`Whoah! A buggy ttf file in the font ${font.typefaceName} ${font.weight} ${font.italic? 'italic' : 'normal'}. You should either fix it or find a new one.`); + window.alertify.error(`Whoah! A buggy ttf file in the font ${font.typefaceName} ${font.weight} ${font.italic ? 'italic' : 'normal'}. You should either fix it or find a new one.`); throw e; } - writePromises.push(fs.copy(`${projdir}/fonts/${font.origname}` , writeDir + '/fonts/' + font.origname)); + writePromises.push(fs.copy(`${projdir}/fonts/${font.origname}`, writeDir + '/fonts/' + font.origname)); writePromises.push(fs.writeFile(writeDir + '/fonts/' + font.origname + '.woff', woff)); css += stringifyFont(font); })); diff --git a/src/node_requires/exporter/html.js b/src/node_requires/exporter/html.js index 347793b4d..afdb7fa5c 100644 --- a/src/node_requires/exporter/html.js +++ b/src/node_requires/exporter/html.js @@ -1,11 +1,10 @@ const substituteHtmlVars = (str, project, injects) => - str - .replace('', injects.htmltop) - .replace('', injects.htmlbottom) - .replace(//g, project.settings.title || 'ct.js game') - .replace(//g, project.settings.accent || 'ct.js game') - .replace('', (project.emitterTandems && project.emitterTandems.length)? '' : '') - .replace('', project.skeletons.some(s => s.from === 'dragonbones')? '' : ''); + str.replace('', injects.htmltop) + .replace('', injects.htmlbottom) + .replace(//g, project.settings.title || 'ct.js game') + .replace(//g, project.settings.accent || 'ct.js game') + .replace('', (project.emitterTandems && project.emitterTandems.length) ? '' : '') + .replace('', project.skeletons.some(s => s.from === 'dragonbones') ? '' : ''); module.exports = { substituteHtmlVars diff --git a/src/node_requires/exporter/icons.js b/src/node_requires/exporter/icons.js index 5060f35df..c19b72a4d 100644 --- a/src/node_requires/exporter/icons.js +++ b/src/node_requires/exporter/icons.js @@ -3,11 +3,12 @@ const png2icons = require('png2icons'); const path = require('path'), fs = require('fs-extra'); -const resizeTo = async function(img, length, dest, soft) { +const resizeTo = async function (img, length, dest, soft) { const canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'); canvas.width = canvas.height = length; - ctx.imageSmoothingQuality = soft? 'high' : 'low'; + ctx.imageSmoothingQuality = soft ? 'high' : 'low'; + // eslint-disable-next-line id-length ctx.imageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = soft; const k = length / Math.max(img.width, img.height); ctx.drawImage( @@ -15,21 +16,21 @@ const resizeTo = async function(img, length, dest, soft) { (length - img.width * k) / 2, (length - img.height * k) / 2, img.width * k, img.height * k ); - var data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); + var iconBase64data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/, ''); + var buf = new Buffer(iconBase64data, 'base64'); await fs.writeFile(dest, buf); }; -const bakeFavicons = async function(proj, writeDir) { +const bakeFavicons = async function (proj, writeDir) { const iconMap = { // 'android-chrome': [36, 48, 72, 96, 192, 256, 384, 512], // maybe, someday 'apple-touch-icon': [57, 60, 72, 76, 114, 120, 144, 152, 167, 180, 1024], - 'coast': [228], - 'favicon': [16, 32, 48, 64], - 'mstile': [150, 310], + coast: [228], + favicon: [16, 32, 48, 64], + mstile: [150, 310], 'yandex-browser': [50] }; const img = await getDOMImage(proj.settings.branding.icon, 'ct_ide.png'), - fsPath = proj.settings.branding.icon? getTextureOrig(proj.settings.branding.icon, true) : path.resolve('ct_ide.png'); + fsPath = proj.settings.branding.icon ? getTextureOrig(proj.settings.branding.icon, true) : path.resolve('ct_ide.png'); const promises = []; const soft = !proj.settings.pixelatedrender; for (const name in iconMap) { @@ -37,11 +38,10 @@ const bakeFavicons = async function(proj, writeDir) { promises.push(resizeTo(img, size, path.join(writeDir, `${name}-${size}x${size}.png`), soft)); } } - promises.push( - fs.readFile(fsPath) - .then(buff => png2icons.createICO(buff, proj.settings.pixelatedrender? png2icons.BILINEAR : png2icons.HERMITE)) - .then(buff => fs.outputFile(path.join(writeDir, 'favicon.ico'), buff)) - ); + const interpolation = proj.settings.pixelatedrender ? png2icons.BILINEAR : png2icons.HERMITE; + promises.push(fs.readFile(fsPath) + .then(buff => png2icons.createICO(buff, interpolation)) + .then(buff => fs.outputFile(path.join(writeDir, 'favicon.ico'), buff))); await Promise.all(promises); }; diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index 44ceac6e0..71d1f8df5 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -14,15 +14,15 @@ const {stringifyTypes} = require('./types'); const {bundleFonts} = require('./fonts'); const {bakeFavicons} = require('./icons'); -const parseKeys = function(data, str, lib) { +const parseKeys = function (catmod, str, lib) { var str2 = str; - if (data.fields) { - for (const field in data.fields) { - const val = currentProject.libs[lib][data.fields[field].key]; - if (data.fields[field].type === 'checkbox' && !val) { - str2 = str2.replace(RegExp('(/\\*)?%' + data.fields[field].key + '%(\\*/)?', 'g'), 'false'); + if (catmod.fields) { + for (const field in catmod.fields) { + const val = currentProject.libs[lib][catmod.fields[field].key]; + if (catmod.fields[field].type === 'checkbox' && !val) { + str2 = str2.replace(RegExp('(/\\*)?%' + catmod.fields[field].key + '%(\\*/)?', 'g'), 'false'); } else { - str2 = str2.replace(RegExp('(/\\*)?%' + data.fields[field].key + '%(\\*/)?', 'g'), val || ''); + str2 = str2.replace(RegExp('(/\\*)?%' + catmod.fields[field].key + '%(\\*/)?', 'g'), val || ''); } } } @@ -31,12 +31,12 @@ const parseKeys = function(data, str, lib) { const addModules = async () => { // async const pieces = await Promise.all(Object.keys(currentProject.libs).map(async lib => { - const data = await fs.readJSON(path.join(basePath + 'ct.libs/', lib, 'module.json'), { - 'encoding': 'utf8' + const moduleJSON = await fs.readJSON(path.join(basePath + 'ct.libs/', lib, 'module.json'), { + encoding: 'utf8' }); if (await fs.pathExists(path.join(basePath + 'ct.libs/', lib, 'index.js'))) { - return parseKeys(data, await fs.readFile(path.join(basePath + 'ct.libs/', lib, 'index.js'), { - 'encoding': 'utf8' + return parseKeys(moduleJSON, await fs.readFile(path.join(basePath + 'ct.libs/', lib, 'index.js'), { + encoding: 'utf8' }), lib); } return ''; @@ -47,7 +47,7 @@ const addModules = async () => { // async const injectModules = injects => // async Promise.all(Object.keys(currentProject.libs).map(async lib => { const libData = await fs.readJSON(path.join(basePath + 'ct.libs/', lib, 'module.json'), { - 'encoding': 'utf8' + encoding: 'utf8' }); if (await fs.pathExists(path.join(basePath + 'ct.libs/', lib, 'injects'))) { const injectFiles = await fs.readdir(path.join(basePath + 'ct.libs/', lib, 'injects')), @@ -69,6 +69,7 @@ const makeWritableDir = async () => { const {getWritableDir} = require('./../platformUtils'); writeDir = path.join(await getWritableDir(), 'export'); }; +// eslint-disable-next-line max-lines-per-function const exportCtProject = async (project, projdir) => { const {languageJSON} = require('./../i18n'); currentProject = project; @@ -87,7 +88,7 @@ const exportCtProject = async (project, projdir) => { const injects = { load: '', start: '', - 'switch': '', + switch: '', oncreate: '', ondestroy: '', @@ -147,7 +148,7 @@ const exportCtProject = async (project, projdir) => { ]; for (const file of sourcesList) { sources[file] = fs.readFile(path.join(basePath, 'ct.release', file), { - 'encoding': 'utf8' + encoding: 'utf8' }); } @@ -269,6 +270,7 @@ const exportCtProject = async (project, projdir) => { const compiler = new ClosureCompiler({ /* eslint-disable camelcase */ compilation_level: 'SIMPLE', + // eslint-disable-next-line id-length use_types_for_optimization: false, jscomp_off: '*', // Disable warnings to not to booo users language_out: 'ECMASCRIPT3', @@ -315,10 +317,10 @@ const exportCtProject = async (project, projdir) => { const csswring = require('csswring'); const htmlMinify = require('html-minifier').minify; const htmlUnminified = fs.readFile(path.join(writeDir, '/index.html'), { - 'encoding': 'utf8' + encoding: 'utf8' }); const cssUnminified = fs.readFile(path.join(writeDir, '/ct.css'), { - 'encoding': 'utf8' + encoding: 'utf8' }); await Promise.all([ fs.writeFile( @@ -336,7 +338,7 @@ const exportCtProject = async (project, projdir) => { await fs.copy(path.join(projdir, '/snd/', sound.origname), path.join(writeDir, '/snd/', sound.uid + ext)); })); - return path.join(writeDir, `/index.${currentProject.settings.minifyhtml? 'min.': ''}html`); + return path.join(writeDir, `/index.${currentProject.settings.minifyhtml ? 'min.' : ''}html`); }; module.exports = exportCtProject; diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index 4d2aef995..a2f1e917b 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -42,9 +42,9 @@ const stringifyRooms = proj => { const texture = glob.texturemap[tile.texture].g; layer.tiles.push({ texture: texture.name, - frame: tile.grid[0] + x + (y+tile.grid[1])*texture.grid[0], - x: tile.x + x*(texture.width + texture.marginx), - y: tile.y + y*(texture.height + texture.marginy), + frame: tile.grid[0] + x + (y + tile.grid[1]) * texture.grid[0], + x: tile.x + x * (texture.width + texture.marginx), + y: tile.y + y * (texture.height + texture.marginy), width: texture.width, height: texture.height }); diff --git a/src/node_requires/exporter/skeletons.js b/src/node_requires/exporter/skeletons.js index 1752fbb11..20333a6fe 100644 --- a/src/node_requires/exporter/skeletons.js +++ b/src/node_requires/exporter/skeletons.js @@ -4,17 +4,17 @@ const basePath = './data/'; const packSkeletons = async (proj, projdir, writeDir) => { const writePromises = []; - const data = { + const exporterData = { loaderScript: 'PIXI.Loader.shared', startScript: 'const dbf = dragonBones.PixiFactory.factory;', registry: {}, requiresDB: false }; if (!proj.skeletons.length) { - data.startScript = ''; - data.loaderScript = ''; - data.registry = JSON.stringify(data.registry); - return data; + exporterData.startScript = ''; + exporterData.loaderScript = ''; + exporterData.registry = JSON.stringify(exporterData.registry); + return exporterData; } for (const skeleton of proj.skeletons) { const slice = skeleton.origname.replace('_ske.json', ''); @@ -22,28 +22,30 @@ const packSkeletons = async (proj, projdir, writeDir) => { writePromises.push(fs.copy(`${projdir}/img/${slice}_tex.json`, `${writeDir}/img/${slice}_tex.json`)); writePromises.push(fs.copy(`${projdir}/img/${slice}_tex.png`, `${writeDir}/img/${slice}_tex.png`)); - data.loaderScript += `.add('${slice}_ske.json', './img/${slice}_ske.json')`; - data.loaderScript += `.add('${slice}_tex.json', './img/${slice}_tex.json')`; - data.loaderScript += `.add('${slice}_tex.png', './img/${slice}_tex.png')`; + exporterData.loaderScript += `.add('${slice}_ske.json', './img/${slice}_ske.json')`; + exporterData.loaderScript += `.add('${slice}_tex.json', './img/${slice}_tex.json')`; + exporterData.loaderScript += `.add('${slice}_tex.png', './img/${slice}_tex.png')`; - data.startScript += `dbf.parseDragonBonesData(PIXI.Loader.shared.resources['${slice}_ske.json'].data);\n`; - data.startScript += `dbf.parseTextureAtlasData(PIXI.Loader.shared.resources['${slice}_tex.json'].data, PIXI.Loader.shared.resources['${slice}_tex.png'].texture);\n`; + exporterData.startScript += `dbf.parseDragonBonesData(PIXI.Loader.shared.resources['${slice}_ske.json'].data);\n`; + exporterData.startScript += `dbf.parseTextureAtlasData(PIXI.Loader.shared.resources['${slice}_tex.json'].data, PIXI.Loader.shared.resources['${slice}_tex.png'].texture);\n`; - data.registry[skeleton.name] = { + exporterData.registry[skeleton.name] = { origname: slice, type: skeleton.from }; if (skeleton.from === 'dragonbones') { - data.requiresDB = true; + exporterData.requiresDB = true; } } - data.loaderScript += ';'; - if (data.requiresDB) { + exporterData.loaderScript += ';'; + if (exporterData.requiresDB) { writePromises.push(fs.copyFile(basePath + 'ct.release/DragonBones.min.js', writeDir + '/DragonBones.min.js')); } - data.registry = JSON.stringify(data.registry); + exporterData.registry = JSON.stringify(exporterData.registry); await Promise.all(writePromises); - return data; + return exporterData; }; -module.exports = {packSkeletons}; +module.exports = { + packSkeletons +}; diff --git a/src/node_requires/exporter/sounds.js b/src/node_requires/exporter/sounds.js index c4f358e5e..5ac888a98 100644 --- a/src/node_requires/exporter/sounds.js +++ b/src/node_requires/exporter/sounds.js @@ -10,9 +10,9 @@ const stringifySounds = proj => { ogg = s.origname.slice(-4) === '.ogg'; sounds += ` ct.sound.init('${s.name}', { - wav: ${wav? '\'./snd/'+s.uid+'.wav\'' : false}, - mp3: ${mp3? '\'./snd/'+s.uid+'.mp3\'' : false}, - ogg: ${ogg? '\'./snd/'+s.uid+'.ogg\'' : false} + wav: ${wav ? '\'./snd/' + s.uid + '.wav\'' : false}, + mp3: ${mp3 ? '\'./snd/' + s.uid + '.mp3\'' : false}, + ogg: ${ogg ? '\'./snd/' + s.uid + '.ogg\'' : false} }, { poolSize: ${s.poolSize || 5}, music: ${Boolean(s.isMusic)} @@ -21,4 +21,6 @@ ct.sound.init('${s.name}', { return sounds; }; -module.exports = {stringifySounds}; +module.exports = { + stringifySounds +}; diff --git a/src/node_requires/exporter/styles.js b/src/node_requires/exporter/styles.js index 599fcb879..3b3923b3c 100644 --- a/src/node_requires/exporter/styles.js +++ b/src/node_requires/exporter/styles.js @@ -13,4 +13,6 @@ ct.styles.new( return styles; }; -module.exports = {stringifyStyles}; +module.exports = { + stringifyStyles +}; diff --git a/src/node_requires/exporter/textures.js b/src/node_requires/exporter/textures.js index 5db3e6d70..3c40d3752 100644 --- a/src/node_requires/exporter/textures.js +++ b/src/node_requires/exporter/textures.js @@ -29,43 +29,151 @@ const getTextureShape = texture => { }; }; +const getTextureFrameCrops = tex => { + const frames = []; + for (var yy = 0; yy < tex.grid[1]; yy++) { + for (var xx = 0; xx < tex.grid[0]; xx++) { + const key = `${tex.name}@frame${tex.grid[0] * yy + xx}`; // PIXI.Texture's name in a shared loader + // Put each frame individually, with a padding on each side + frames.push({ + // eslint-disable-next-line id-blacklist + data: { + name: tex.name, + tex, + frame: {// A crop from the source texture + x: tex.offx + xx * (tex.width + tex.marginx), + y: tex.offy + yy * (tex.height + tex.marginy), + width: tex.width, + height: tex.height + }, + key + }, + width: tex.width + tex.padding * 2, + height: tex.height + tex.padding * 2 + }); + // skip unnecessary frames when tex.untill is set + // eslint-disable-next-line max-depth + if (yy * tex.grid[0] + xx >= tex.untill && tex.untill > 0) { + break; + } + } + } + return frames; +}; + +// eslint-disable-next-line max-lines-per-function +const drawAtlasFromBin = (bin, binInd) => { + const atlas = document.createElement('canvas'); + atlas.width = bin.width; + atlas.height = bin.height; + atlas.x = atlas.getContext('2d'); + atlas.x.imageSmoothingQuality = 'low'; + // eslint-disable-next-line id-length + atlas.x.imageSmoothingEnabled = atlas.x.webkitImageSmoothingEnabled = false; + + const atlasJSON = { + meta: { + app: 'https://ctjs.rocks/', + version: process.versions.ctjs, + image: `a${binInd}.png`, + format: 'RGBA8888', + size: { + w: bin.width, + h: bin.height + }, + scale: '1' + }, + frames: {}, + animations: {} + }; + for (const block of bin.rects) { + const {tex} = block.data, + {frame} = block.data, + {key} = block.data, + img = glob.texturemap[tex.uid]; + const p = tex.padding; + // draw the main crop rectangle + atlas.x.drawImage( + img, + frame.x, frame.y, frame.width, frame.height, + block.x + p, block.y + p, frame.width, frame.height + ); + // repeat the left side of the image + atlas.x.drawImage( + img, + frame.x, frame.y, 1, frame.height, + block.x, block.y + p, p, frame.height + ); + // repeat the right side of the image + atlas.x.drawImage( + img, + frame.x + frame.width - 1, frame.y, 1, frame.height, + block.x + frame.width + p, block.y + p, p, frame.height + ); + // repeat the top side of the image + atlas.x.drawImage( + img, + frame.x, frame.y, frame.width, 1, + block.x + p, block.y, frame.width, p + ); + // repeat the bottom side of the image + atlas.x.drawImage( + img, + frame.x, frame.y + frame.height - 1, frame.width, 1, + block.x + p, block.y + frame.height + p, frame.width, p + ); + // A multi-frame sprite + const keys = []; + keys.push(key); + atlasJSON.frames[key] = { + frame: { + x: block.x + p, + y: block.y + p, + w: frame.width, + h: frame.height + }, + rotated: false, + trimmed: false, + spriteSourceSize: { + x: 0, + y: 0, + w: tex.width, + h: tex.height + }, + sourceSize: { + w: tex.width, + h: tex.height + }, + anchor: { + x: tex.axis[0] / tex.width, + y: tex.axis[1] / tex.height + } + }; + } + + return { + canvas: atlas, + json: atlasJSON + }; +}; + const packImages = async (proj, writeDir) => { - const blocks = []; - const tiledImages = []; - const keys = {}; // a collection of frame names for each texture name + const blocks = [], + tiledImages = [], + keys = {}; // A collection of frame names for each texture name + // It is then used for ct.res to create animation sequences - // Write functions will be run in parallel, and this array will block the finalization of the function + // Write functions will be run in parallel, + // and this array will block the finalization of the function const writePromises = []; for (const tex of proj.textures) { if (!tex.tiled) { keys[tex.origname] = []; - for (var yy = 0; yy < tex.grid[1]; yy++) { - for (var xx = 0; xx < tex.grid[0]; xx++) { - const key = `${tex.name}@frame${tex.grid[0] * yy + xx}`; // PIXI.Texture's name in a shared loader - // Put each frame individually, with 1px padding on each side - blocks.push({ - data: { - name: tex.name, - tex, - frame: {// A crop from the source texture - x: tex.offx + xx * (tex.width + tex.marginx), - y: tex.offy + yy * (tex.height + tex.marginy), - width: tex.width, - height: tex.height - }, - key, - }, - width: tex.width + tex.padding * 2, - height: tex.height + tex.padding * 2, - }); - keys[tex.origname].push(key); - // skip unnecessary frames when tex.untill is set - // eslint-disable-next-line max-depth - if (yy * tex.grid[0] + xx >= tex.untill && tex.untill > 0) { - break; - } - } + const frames = getTextureFrameCrops(tex); + for (const frame of frames) { + blocks.push(frame); + keys[tex.origname].push(frame.data.key); } } else { tiledImages.push({ @@ -82,103 +190,24 @@ const packImages = async (proj, writeDir) => { const atlases = []; // names of atlases' json files const Packer = require('maxrects-packer').MaxRectsPacker; const atlasWidth = 2048, - atlasHeight = 2048; + atlasHeight = 2048; const pack = new Packer(atlasWidth, atlasHeight, 0); // pack all the frames pack.addArray(blocks); // get all atlases - pack.bins.forEach((bin, binInd) => { - const atlas = document.createElement('canvas'); - atlas.width = bin.width; - atlas.height = bin.height; - atlas.x = atlas.getContext('2d'); - atlas.x.imageSmoothingQuality = 'low'; - atlas.x.imageSmoothingEnabled = atlas.x.webkitImageSmoothingEnabled = false; - - const atlasJSON = { - meta: { - app: 'https://ctjs.rocks/', - version: process.versions.ctjs, - image: `a${binInd}.png`, - format: 'RGBA8888', - size: { - w: bin.width, - h: bin.height - }, - scale: '1' - }, - frames: {}, - animations: {} - }; - for (const block of bin.rects) { - const {tex} = block.data, - {frame} = block.data, - {key} = block.data, - img = glob.texturemap[tex.uid]; - const p = tex.padding; - // draw the main crop rectangle - atlas.x.drawImage(img, - frame.x, frame.y, frame.width, frame.height, - block.x+p, block.y+p, frame.width, frame.height - ); - // repeat the left side of the image - atlas.x.drawImage(img, - frame.x, frame.y, 1, frame.height, - block.x, block.y+p, p, frame.height - ); - // repeat the right side of the image - atlas.x.drawImage(img, - frame.x+frame.width-1, frame.y, 1, frame.height, - block.x+frame.width+p, block.y+p, p, frame.height - ); - // repeat the top side of the image - atlas.x.drawImage(img, - frame.x, frame.y, frame.width, 1, - block.x+p, block.y, frame.width, p - ); - // repeat the bottom side of the image - atlas.x.drawImage(img, - frame.x, frame.y+frame.height-1, frame.width, 1, - block.x+p, block.y+frame.height+p, frame.width, p - ); - // A multi-frame sprite - const keys = []; - keys.push(key); - atlasJSON.frames[key] = { - frame: { - x: block.x+p, - y: block.y+p, - w: frame.width, - h: frame.height - }, - rotated: false, - trimmed: false, - spriteSourceSize: { - x: 0, - y: 0, - w: tex.width, - h: tex.height - }, - sourceSize: { - w: tex.width, - h: tex.height - }, - anchor: { - x: tex.axis[0] / tex.width, - y: tex.axis[1] / tex.height - } - }; - } - writePromises.push(fs.outputJSON(`${writeDir}/img/a${binInd}.json`, atlasJSON)); - res += `\n.add('./img/a${binInd}.json')`; - var data = atlas.toDataURL().replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); - writePromises.push(fs.writeFile(`${writeDir}/img/a${binInd}.png`, buf)); - atlases.push(`./img/a${binInd}.json`); + pack.bins.map(drawAtlasFromBin).forEach((atlas, ind) => { + writePromises.push(fs.outputJSON(`${writeDir}/img/a${ind}.json`, atlas.json)); + res += `\n.add('./img/a${ind}.json')`; + var atlasBase64 = atlas.canvas.toDataURL().replace(/^data:image\/\w+;base64,/, ''); + var buf = new Buffer(atlasBase64, 'base64'); + writePromises.push(fs.writeFile(`${writeDir}/img/a${ind}.png`, buf)); + atlases.push(`./img/a${ind}.json`); }); for (const tex of proj.textures) { registry[tex.name] = { - frames: tex.untill > 0? Math.min(tex.untill, tex.grid[0]*tex.grid[1]) : tex.grid[0]*tex.grid[1], + frames: tex.untill > 0 ? + Math.min(tex.untill, tex.grid[0] * tex.grid[1]) : + tex.grid[0] * tex.grid[1], shape: getTextureShape(tex), anchor: { x: tex.axis[0] / tex.width, @@ -188,8 +217,8 @@ const packImages = async (proj, writeDir) => { } for (let i = 0, l = tiledImages.length; i < l; i++) { const atlas = document.createElement('canvas'), - {tex} = tiledImages[i], - img = glob.texturemap[tex.uid]; + {tex} = tiledImages[i], + img = glob.texturemap[tex.uid]; atlas.x = atlas.getContext('2d'); atlas.width = tex.width; atlas.height = tex.height; @@ -218,4 +247,6 @@ const packImages = async (proj, writeDir) => { }; }; -module.exports = {packImages}; +module.exports = { + packImages +}; diff --git a/src/node_requires/exporter/types.js b/src/node_requires/exporter/types.js index d66f9a30b..493004709 100644 --- a/src/node_requires/exporter/types.js +++ b/src/node_requires/exporter/types.js @@ -1,6 +1,6 @@ const textures = require('../resources/textures'); -const stringifyTypes = function(proj) { +const stringifyTypes = function (proj) { /* Stringify types */ var types = ''; for (const k in proj.types) { @@ -8,7 +8,7 @@ const stringifyTypes = function(proj) { types += ` ct.types.templates["${type.name}"] = { depth: ${type.depth}, - ${type.texture !== -1? 'texture: "'+ textures.getTextureFromId(type.texture).name + '",' : ''} + ${type.texture !== -1 ? 'texture: "' + textures.getTextureFromId(type.texture).name + '",' : ''} onStep: function () { ${type.onstep} }, @@ -23,10 +23,11 @@ ct.types.templates["${type.name}"] = { }, extends: ${JSON.stringify(type.extends || {})} }; -ct.types.list['${type.name}'] = [];` - ; +ct.types.list['${type.name}'] = [];`; } return types; }; -module.exports = {stringifyTypes}; +module.exports = { + stringifyTypes +}; diff --git a/src/node_requires/generateGUID.js b/src/node_requires/generateGUID.js index ed74c205c..e15abfa44 100644 --- a/src/node_requires/generateGUID.js +++ b/src/node_requires/generateGUID.js @@ -1,9 +1,11 @@ +/* eslint-disable func-names */ +/* eslint-disable no-mixed-operators */ /* eslint-disable no-bitwise */ /** * @see https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#2117523 * @returns {String} An RFC4122 version 4 compliant GUID */ -const generateGUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { +const generateGUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); diff --git a/src/node_requires/generators/gridTexture.js b/src/node_requires/generators/gridTexture.js index b1fe6ed7b..6b2d84ee3 100644 --- a/src/node_requires/generators/gridTexture.js +++ b/src/node_requires/generators/gridTexture.js @@ -1,5 +1,4 @@ -const generateCanvasGrid = function(size, color) { - +const generateCanvasGrid = function (size, color) { color = color || '#666'; if (typeof size === 'number') { size = [size, size]; @@ -24,7 +23,7 @@ const generateCanvasGrid = function(size, color) { return canvas; }; -const generatePixiTextureGrid = function(size, color) { +const generatePixiTextureGrid = function (size, color) { const canvas = generateCanvasGrid(size, color); return PIXI.Texture.from(canvas); }; diff --git a/src/node_requires/hotkeys.js b/src/node_requires/hotkeys.js index f3e81cd0c..ad6c7c88d 100644 --- a/src/node_requires/hotkeys.js +++ b/src/node_requires/hotkeys.js @@ -1,11 +1,11 @@ /* From @github/hotkey see https://github.com/github/hotkey/ */ -const isFormField = function(element) { - if (!(element instanceof HTMLElement)) { +const isFormField = function (inputElement) { + if (!(inputElement instanceof HTMLElement)) { return false; } - var name = element.nodeName.toLowerCase(); - var type = (element.getAttribute('type') || '').toLowerCase(); + var name = inputElement.nodeName.toLowerCase(); + var type = (inputElement.getAttribute('type') || '').toLowerCase(); /* eslint no-mixed-operators: off*/ return name === 'select' || name === 'textarea' || @@ -14,12 +14,12 @@ const isFormField = function(element) { type !== 'reset' && type !== 'checkbox' && type !== 'radio' || - element.isContentEditable; + inputElement.isContentEditable; }; const getCode = e => '' - .concat(e.ctrlKey? 'Control+' : '') - .concat(e.altKey? 'Alt+' : '') + .concat(e.ctrlKey ? 'Control+' : '') + .concat(e.altKey ? 'Alt+' : '') .concat(e.metaKey ? 'Meta+' : '') .concat(e.key); @@ -139,7 +139,7 @@ class Hotkeys { } } -module.exports = function (doc) { +module.exports = function mountHotkeys(doc) { doc = doc || document; if (!doc) { throw new Error('Can\'t find the document object! Am I in a bare node.js context?!'); diff --git a/src/node_requires/jellify.js b/src/node_requires/jellify.js index a285981fe..4d0c5171b 100644 --- a/src/node_requires/jellify.js +++ b/src/node_requires/jellify.js @@ -7,14 +7,14 @@ const jelloEnded = Symbol('jelloEnded'); * @param {HTMLElement} element The element that should be animated * @returns {void} */ -module.exports = function(element) { - if (jelloEnded in element) { +module.exports = function jellify(htmlTag) { + if (jelloEnded in htmlTag) { return; } - element[jelloEnded] = false; - element.classList.add('jello'); + htmlTag[jelloEnded] = false; + htmlTag.classList.add('jello'); setTimeout(() => { - element.classList.remove('jello'); - delete element[jelloEnded]; + htmlTag.classList.remove('jello'); + delete htmlTag[jelloEnded]; }, 1000); }; diff --git a/src/node_requires/objectUtils.js b/src/node_requires/objectUtils.js index 9c063c637..57efd28f0 100644 --- a/src/node_requires/objectUtils.js +++ b/src/node_requires/objectUtils.js @@ -3,7 +3,7 @@ * @module */ -const extend = function(destination, source) { +const extend = function (destination, source) { for (var property in source) { if (destination[property] && typeof destination[property] === 'object' && @@ -24,7 +24,7 @@ const extend = function(destination, source) { * @param {Object|Array} source The object from which to copy new properties * @returns {Object|Array} The extended destination object */ -const extendValid = function(destination, source) { +const extendValid = function (destination, source) { /* Considering JSON-valid objects */ for (const key in source) { // it is either a generic object or an array @@ -46,7 +46,7 @@ const extendValid = function(destination, source) { return destination; }; -const equal = function(one, two) { +const equal = function (one, two) { for (const property in one) { if (one[property] !== two[property]) { return false; diff --git a/src/node_requires/platformUtils.js b/src/node_requires/platformUtils.js index 64e27709b..9133c8439 100644 --- a/src/node_requires/platformUtils.js +++ b/src/node_requires/platformUtils.js @@ -37,7 +37,7 @@ const mod = { async getWritableDir() { const path = require('path'); - const exec = path.dirname(process.cwd()).replace(/\\/g,'/'); + const exec = path.dirname(process.cwd()).replace(/\\/g, '/'); // The `HOME` variable is not always available in ct.js on Windows const home = process.env.HOME || ((process.env.HOMEDRIVE || '') + process.env.HOMEPATH); diff --git a/src/node_requires/resources/textures.js b/src/node_requires/resources/textures.js index c867c5b88..4e15df039 100644 --- a/src/node_requires/resources/textures.js +++ b/src/node_requires/resources/textures.js @@ -26,9 +26,9 @@ const getTexturePreview = function (texture, x2, fs) { texture = getTextureFromId(texture); } if (fs) { - return `${global.projdir}/img/${texture.origname}_prev${x2? '@2' : ''}.png`; + return `${global.projdir}/img/${texture.origname}_prev${x2 ? '@2' : ''}.png`; } - return `file://${global.projdir}/img/${texture.origname}_prev${x2? '@2' : ''}.png?cache=${texture.lastmod}`; + return `file://${global.projdir}/img/${texture.origname}_prev${x2 ? '@2' : ''}.png?cache=${texture.lastmod}`; }; /** @@ -37,7 +37,7 @@ const getTexturePreview = function (texture, x2, fs) { * @param {boolean} [fs] If set to true, returns a file system path, not a URI. * @returns {string} The full path to the source image. */ -const getTextureOrig = function(texture, fs) { +const getTextureOrig = function (texture, fs) { if (texture === -1) { return 'data/img/notexture.png'; } @@ -50,7 +50,7 @@ const getTextureOrig = function(texture, fs) { return `file://${global.projdir}/img/${texture.origname}?cache=${texture.lastmod}`; }; -const loadBaseTextureForCtTexture = texture => new Promise((resolve, reject) => { +const baseTextureFromTexture = texture => new Promise((resolve, reject) => { const textureLoader = new PIXI.Loader(); const {resources} = textureLoader; @@ -73,9 +73,9 @@ const clearPixiTextureCache = function () { * @param {any} tex A ct.js texture object * @returns {Array} An array of PIXI.Textures */ -const textureArrayFromCtTexture = async function (tex) { +const texturesFromCtTexture = async function (tex) { const frames = []; - const baseTexture = await loadBaseTextureForCtTexture(tex); + const baseTexture = await baseTextureFromTexture(tex); for (let col = 0; col < tex.grid[1]; col++) { for (let row = 0; row < tex.grid[0]; row++) { const texture = new PIXI.Texture( @@ -87,7 +87,10 @@ const textureArrayFromCtTexture = async function (tex) { tex.height ) ); - texture.defaultAnchor = new PIXI.Point(tex.axis[0] / tex.width, tex.axis[1] / tex.height); + texture.defaultAnchor = new PIXI.Point( + tex.axis[0] / tex.width, + tex.axis[1] / tex.height + ); frames.push(texture); if (col * tex.grid[0] + row >= tex.grid.untill && tex.grid.untill > 0) { break; @@ -99,7 +102,7 @@ const textureArrayFromCtTexture = async function (tex) { let defaultTexture; -const getDOMImage = function(texture, deflt) { +const getDOMImage = function (texture, deflt) { let path; const img = document.createElement('img'); if (texture === -1 || !texture) { @@ -140,8 +143,9 @@ const getPixiTexture = async function (texture, frame, allowMinusOne) { if (!pixiTextureCache[uid] || pixiTextureCache[uid].lastmod !== texture.lastmod ) { - const tex = await textureArrayFromCtTexture(texture); - // Everything is constant, and the key gets overridden. Where's the race condition? False positive?? + const tex = await texturesFromCtTexture(texture); + // Everything is constant, and the key gets overridden. + // Where's the race condition? False positive?? // eslint-disable-next-line require-atomic-updates pixiTextureCache[uid] = { lastmod: texture.lastmod, @@ -159,7 +163,7 @@ const getPixiTexture = async function (texture, frame, allowMinusOne) { * @param {string} name The name of the texture. * @return {boolean} True if the texture exists. */ -const getTextureFromName = function(name) { +const getTextureFromName = function (name) { const texture = global.currentProject.textures.find(tex => tex.name === name); if (!texture) { throw new Error(`Attempt to get a non-existent texture with name ${name}`); @@ -172,7 +176,8 @@ const getTextureFromName = function(name) { * @param {string} source Path to the image * @param {string} destFile Path to the destinating image * @param {number} size Size of the square thumbnail, in pixels - * @returns {Promise} Resolves after creating a thumbnail. On success, passes `destFile`, the path to the created thumbnail. + * @returns {Promise} Resolves after creating a thumbnail. + * On success, passes `destFile`, the path to the created thumbnail. */ const imgGenPreview = (source, destFile, size) => { const thumbnail = document.createElement('img'); @@ -180,7 +185,7 @@ const imgGenPreview = (source, destFile, size) => { return new Promise((accept, reject) => { thumbnail.onload = () => { var c = document.createElement('canvas'), - w, h, k; + w, h, k; c.x = c.getContext('2d'); c.width = c.height = size; c.x.clearRect(0, 0, size, size); @@ -196,15 +201,15 @@ const imgGenPreview = (source, destFile, size) => { } c.x.drawImage( thumbnail, - (size - thumbnail.width*k)/2, - (size - thumbnail.height*k)/2, - thumbnail.width*k, - thumbnail.height*k + (size - thumbnail.width * k) / 2, + (size - thumbnail.height * k) / 2, + thumbnail.width * k, + thumbnail.height * k ); // strip off the data:image url prefix to get just the base64-encoded bytes var dataURL = c.toDataURL(); - var data = dataURL.replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); + var base64data = dataURL.replace(/^data:image\/\w+;base64,/, ''); + var buf = new Buffer(base64data, 'base64'); var stream = fs.createWriteStream(destFile); stream.on('finish', () => { setTimeout(() => { // WHY THE HECK I EVER NEED THIS?! @@ -250,10 +255,11 @@ const importImageToTexture = async src => { imgGenPreview(dest, dest + '_prev.png', 64), imgGenPreview(dest, dest + '_prev@2.png', 128) ]); + const texName = path.basename(src) + .replace(/\.(jpg|gif|png|jpeg)/gi, '') + .replace(/\s/g, '_'); const obj = { - name: path.basename(src) - .replace(/\.(jpg|gif|png|jpeg)/gi, '') - .replace(/\s/g, '_'), + name: texName, untill: 0, grid: [1, 1], axis: [0, 0], diff --git a/src/node_requires/styleUtils.js b/src/node_requires/styleUtils.js index 6f63cf7d5..c2fbf2645 100644 --- a/src/node_requires/styleUtils.js +++ b/src/node_requires/styleUtils.js @@ -3,7 +3,7 @@ const mod = { const o = { fontFamily: s.font.family, fontSize: s.font.size, - fontStyle: s.font.italic? 'italic' : 'normal', + fontStyle: s.font.italic ? 'italic' : 'normal', fontWeight: s.font.weight, align: s.font.halign, lineJoin: 'round', diff --git a/src/pug/.eslintrc.json b/src/pug/.eslintrc.json new file mode 100644 index 000000000..23c30dfab --- /dev/null +++ b/src/pug/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "eol-last": "off" + } +} diff --git a/src/pug/debuggerToolbar.pug b/src/pug/debuggerToolbar.pug index 354cdf855..9d7822292 100644 --- a/src/pug/debuggerToolbar.pug +++ b/src/pug/debuggerToolbar.pug @@ -7,4 +7,4 @@ html#theDebuggerToolbarWindow script. document.title = 'Ct.js debugger\'s toolbar'; window.signals = window.signals || riot.observable({}); - riot.mount('*'); \ No newline at end of file + riot.mount('*'); diff --git a/src/pug/includes/head.pug b/src/pug/includes/head.pug index add443f06..e917137a7 100644 --- a/src/pug/includes/head.pug +++ b/src/pug/includes/head.pug @@ -1,12 +1,12 @@ -head - title ct.js - - meta(charset='utf-8') - meta(name='description', content='IDE') - meta(name='viewport', content='width=device-width, initial-scale=1') - - link(href='data/themeDay.css' id="themeCSS" rel='stylesheet') - script(type="text/javascript"). - localStorage.UItheme = localStorage.UItheme || 'Day'; - var theme = localStorage.UItheme; - document.getElementById('themeCSS').href = `data/theme${theme}.css`; \ No newline at end of file +head + title ct.js + + meta(charset='utf-8') + meta(name='description', content='IDE') + meta(name='viewport', content='width=device-width, initial-scale=1') + + link(href='data/themeDay.css' id="themeCSS" rel='stylesheet') + script(type="text/javascript"). + localStorage.UItheme = localStorage.UItheme || 'Day'; + var theme = localStorage.UItheme; + document.getElementById('themeCSS').href = `data/theme${theme}.css`; diff --git a/src/pug/index.pug b/src/pug/index.pug index 3f63ccf66..fc0b22061 100644 --- a/src/pug/index.pug +++ b/src/pug/index.pug @@ -1,73 +1,74 @@ -doctype html -html - include includes/head.pug - body.maximized(data-manage-window) - root-tag - script. - try { - require('gulp'); - console.log(` - ╭──────────────────────────────────────────╮ - │ ├──╮ - │ O-o-oh, a developer! │ │ - │ │ │ - │ If you have recently pulled changes │ │ - │ and face issues unseen before, run │ │ - │ this command in your console: │ │ - │ │ │ - │ $ gulp -f devSetup.gulpfile.js │ │ - │ │ │ - ╰─┬────────────────────────────────────────╯ │ - ╰───────────────────────────────────────────╯ - `); - } catch (e) { - void e; - } - script. - process.versions.ctjs = require('./package.json').version; - script(src="node_modules/pixi.js-legacy/dist/pixi-legacy.min.js") - script(src="data/ct.release/DragonBones.min.js") - script(src="node_modules/pixi-particles/dist/pixi-particles.min.js") - script. - /* So that WebGL contexts are taken from one page, - even if PIXI was called from the background page, - which is used for node modules - */ - global.PIXI = PIXI; - script. - // A polyfill for different nw.js versions - if (!window.__dirname) { - window.__dirname = global.__dirname; - } - include includes/footer.pug - script. - // @see https://github.com/microsoft/monaco-editor-samples/blob/master/nwjs-amd-v2/index.html - (function() { - var ERequire = require('monaco-editor/min/vs/loader.js'); - //__dirname == root path of you application - ERequire.config({ - baseUrl: 'file:///'+global.__dirname.replace(/\\/g, '\\\\')+'/node_modules/monaco-editor/min/' - }); - // workaround monaco-css not understanding the environment - self.module = undefined; - // workaround monaco-typescript not understanding the environment - self.process.browser = true; - ERequire(['vs/editor/editor.main'], function() { - const tomorrow = require('./data/node_requires/monaco-themes/tomorrow.json'); - const horizon = require('./data/node_requires/monaco-themes/horizon.json'); - const ambiance = require('./data/node_requires/monaco-themes/ambiance.json'); - window.monaco = global.monaco; - monaco.editor.defineTheme('tomorrow', tomorrow); - monaco.editor.defineTheme('horizon', horizon); - monaco.editor.defineTheme('ambiance', ambiance); - window.signals = window.signals || riot.observable({}); - window.signals.trigger('monacoBooted'); - }); - })(); - script. - 'use strict'; - window.signals = window.signals || riot.observable({}); - riot.mount('*'); - setTimeout(() => { - document.getElementById('loading').classList.add('fadeout'); - }, 0) +doctype html +html + include includes/head.pug + body.maximized(data-manage-window) + root-tag + script. + try { + require('gulp'); + /* eslint no-console: 0 */ + console.log(` + ╭──────────────────────────────────────────╮ + │ ├──╮ + │ O-o-oh, a developer! │ │ + │ │ │ + │ If you have recently pulled changes │ │ + │ and face issues unseen before, run │ │ + │ this command in your console: │ │ + │ │ │ + │ $ gulp -f devSetup.gulpfile.js │ │ + │ │ │ + ╰─┬────────────────────────────────────────╯ │ + ╰───────────────────────────────────────────╯ + `); + } catch (e) { + void e; + } + script. + process.versions.ctjs = require('./package.json').version; + script(src="node_modules/pixi.js-legacy/dist/pixi-legacy.min.js") + script(src="data/ct.release/DragonBones.min.js") + script(src="node_modules/pixi-particles/dist/pixi-particles.min.js") + script. + /* So that WebGL contexts are taken from one page, + even if PIXI was called from the background page, + which is used for node modules + */ + global.PIXI = PIXI; + script. + // A polyfill for different nw.js versions + if (!window.__dirname) { + window.__dirname = global.__dirname; + } + include includes/footer.pug + script. + // @see https://github.com/microsoft/monaco-editor-samples/blob/master/nwjs-amd-v2/index.html + (function loadMonaco() { + var eRequire = require('monaco-editor/min/vs/loader.js'); + // __dirname == root path of you application + eRequire.config({ + baseUrl: 'file:///' + global.__dirname.replace(/\\/g, '\\\\') + '/node_modules/monaco-editor/min/' + }); + // workaround monaco-css not understanding the environment + self.module = void 0; + // workaround monaco-typescript not understanding the environment + self.process.browser = true; + eRequire(['vs/editor/editor.main'], function onMonacoLoad() { + const tomorrow = require('./data/node_requires/monaco-themes/tomorrow.json'); + const horizon = require('./data/node_requires/monaco-themes/horizon.json'); + const ambiance = require('./data/node_requires/monaco-themes/ambiance.json'); + window.monaco = global.monaco; + monaco.editor.defineTheme('tomorrow', tomorrow); + monaco.editor.defineTheme('horizon', horizon); + monaco.editor.defineTheme('ambiance', ambiance); + window.signals = window.signals || riot.observable({}); + window.signals.trigger('monacoBooted'); + }); + })(); + script. + 'use strict'; + window.signals = window.signals || riot.observable({}); + riot.mount('*'); + setTimeout(() => { + document.getElementById('loading').classList.add('fadeout'); + }, 0); diff --git a/src/pug/qrCodePanel.pug b/src/pug/qrCodePanel.pug index f2b708094..85751207d 100644 --- a/src/pug/qrCodePanel.pug +++ b/src/pug/qrCodePanel.pug @@ -7,4 +7,4 @@ html#theDebuggerQrWindow script. document.title = 'Ct.js networking'; window.signals = window.signals || riot.observable({}); - riot.mount('*'); \ No newline at end of file + riot.mount('*'); diff --git a/src/riotTags/.eslintrc.json b/src/riotTags/.eslintrc.json new file mode 100644 index 000000000..23c30dfab --- /dev/null +++ b/src/riotTags/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "eol-last": "off" + } +} diff --git a/src/riotTags/actions-editor.tag b/src/riotTags/actions-editor.tag index 4f84b7619..0828134f2 100644 --- a/src/riotTags/actions-editor.tag +++ b/src/riotTags/actions-editor.tag @@ -65,7 +65,7 @@ actions-editor.panel.view.pad this.namespace = 'actionsEditor'; this.mixin(window.riotVoc); this.mixin(window.riotWired); - this.addNewAction = e => { + this.addNewAction = () => { global.currentProject.actions.push({ name: 'NewAction', methods: [] @@ -87,17 +87,18 @@ actions-editor.panel.view.pad action.methods.splice(ind, 1); }; this.checkActionNameAndSave = e => { - console.log(e); this.nameTaken = void 0; e.item.action.name = e.currentTarget.value.trim(); - if (global.currentProject.actions.find(action => action !== e.item.action && action.name === e.item.action.name)) { + const existingAction = global.currentProject.actions.find(action => + action !== e.item.action && action.name === e.item.action.name); + if (existingAction) { this.nameTaken = e.item.action.name; } }; - this.saveActions = e => { + this.saveActions = () => { if ((Array.isArray(this.refs.errors) && this.refs.errors.length) || this.refs.errors) { - let errors = this.refs.errors; + let {errors} = this.refs; if (!Array.isArray(errors)) { errors = [errors]; } @@ -111,4 +112,5 @@ actions-editor.panel.view.pad } this.parent.editingActions = false; this.parent.update(); + return true; }; \ No newline at end of file diff --git a/src/riotTags/debugger/debugger-modal.tag b/src/riotTags/debugger/debugger-modal.tag index 26813993a..4e15887ee 100644 --- a/src/riotTags/debugger/debugger-modal.tag +++ b/src/riotTags/debugger/debugger-modal.tag @@ -15,7 +15,7 @@ debugger-modal.view Object.keys(interfaces).forEach(ifname => { var alias = 0; interfaces[ifname].forEach(iface => { - if ('IPv4' !== iface.family || iface.internal !== false) { + if (iface.family !== 'IPv4' || iface.internal !== false) { // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses return; } @@ -40,18 +40,19 @@ debugger-modal.view Day: ['#446adb', '#ffffff'], Night: ['#121822', '#44dbb5'], Horizon: ['#1C1E26', '#E95378'] - } + }; this.on('mount', () => { setTimeout(() => { - for (div of (Array.isArray(this.refs.qr)? this.refs.qr : [this.refs.qr])) { - console.log(div); - var qrcode = new QRCode(div, { + for (const div of (Array.isArray(this.refs.qr) ? this.refs.qr : [this.refs.qr])) { + const themedColors = palette[localStorage.UItheme]; + // eslint-disable-next-line no-new + new QRCode(div, { text: div.getAttribute('data-address'), width: 256, height: 256, - colorDark : palette[localStorage.UItheme]? palette[localStorage.UItheme][0] : palette.Day[0], - colorLight : palette[localStorage.UItheme]? palette[localStorage.UItheme][1] : palette.Day[1], - correctLevel : QRCode.CorrectLevel.H + colorDark: themedColors ? themedColors[0] : palette.Day[0], + colorLight: themedColors ? themedColors[1] : palette.Day[1], + correctLevel: QRCode.CorrectLevel.H }); } }, 0); diff --git a/src/riotTags/debugger/debugger-screen.tag b/src/riotTags/debugger/debugger-screen.tag index 70aa72a58..0debde4bc 100644 --- a/src/riotTags/debugger/debugger-screen.tag +++ b/src/riotTags/debugger/debugger-screen.tag @@ -74,7 +74,8 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical this.width = Math.max(minSizeW, Math.min(getMaxSizeW(), localStorage.debuggerWidth || 500)); this.height = Math.max(minSizeH, Math.min(getMaxSizeH(), localStorage.debuggerHeight || 300)); - // iframes and webviews capture mousemove events needed for resize gutter; this overlay will prevent it + // iframes and webviews capture mousemove events needed for resize gutter; + // this overlay will prevent it const catcher = document.createElement('div'); const s = catcher.style; s.position = 'fixed'; @@ -82,9 +83,9 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical s.zIndex = 100; s.cursor = 'ew-resize'; - this.gutterMouseDown = e => { + this.gutterMouseDown = () => { this.dragging = true; - s.cursor = this.verticalLayout? 'ew-resize' : 'ns-resize'; + s.cursor = this.verticalLayout ? 'ew-resize' : 'ns-resize'; document.body.appendChild(catcher); }; document.addEventListener('mousemove', e => { @@ -112,11 +113,11 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical /* Bootstrap preview and debug views */ this.on('mount', () => { - this.refs.gameView.addEventListener('contentload', e => { + this.refs.gameView.addEventListener('contentload', () => { this.refs.gameView.showDevTools(true, this.refs.devtoolsView); setTimeout(() => { this.refs.devtoolsView.executeScript({ - code: `DevToolsAPI.showPanel('console')`, + code: 'DevToolsAPI.showPanel(\'console\')', mainWorld: true }); }, 1000); @@ -136,7 +137,7 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical const menu = new nw.Menu(); // Query for in-game rooms this.refs.gameView.executeScript({ - code: `JSON.stringify(Object.keys(ct.rooms.templates));`, + code: 'JSON.stringify(Object.keys(ct.rooms.templates));', mainWorld: true }, rooms => { JSON.parse(rooms).map(room => ({ @@ -144,13 +145,14 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical click: () => { this.switchRoom(room); } - })).forEach(entry => menu.append(new nw.MenuItem(entry))); + })) + .forEach(entry => menu.append(new nw.MenuItem(entry))); menu.popup(e.clientX, e.clientY); }); }; /* Buttons' event listeners */ - this.togglePause = e => { + this.togglePause = () => { this.refs.gameView.executeScript({ code: ` if (PIXI.Ticker.shared.started) { @@ -162,7 +164,7 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical `, mainWorld: true }, paused => { - if (paused == 'false') { + if (paused === 'false' || paused === false) { this.gamePaused = false; } else { this.gamePaused = true; @@ -170,16 +172,16 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical this.update(); }); }; - this.restartGame = e => { + this.restartGame = () => { this.refs.gameView.reload(); }; - this.restartRoom = e => { + this.restartRoom = () => { this.refs.gameView.executeScript({ code: 'ct.rooms.switch(ct.room.name);', mainWorld: true }); }; - this.makeScreenshot = e => { + this.makeScreenshot = () => { this.refs.gameView.executeScript({ code: ` var renderTexture = PIXI.RenderTexture.create({ @@ -202,24 +204,24 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical if (!filename) { return; } - const data = dataURL.replace(/^data:image\/\w+;base64,/, ''); - const buf = Buffer.from(data, 'base64'); + const screenshotBase64 = dataURL.replace(/^data:image\/\w+;base64,/, ''); + const buf = Buffer.from(screenshotBase64, 'base64'); const stream = fs.createWriteStream(filename); stream.end(buf); - }) + }); }); }; - this.toggleFullscreen = e => { + this.toggleFullscreen = () => { this.gameFullscreen = !this.gameFullscreen; if (this.gameFullscreen) { - this.previewWindow.enterFullscreen() + this.previewWindow.enterFullscreen(); } else { this.previewWindow.leaveFullscreen(); } }; - this.openQrCodes = e => { + this.openQrCodes = () => { this.showNetworkingModal = !this.showNetworkingModal; }; - this.openExternal = e => { + this.openExternal = () => { nw.Shell.openExternal(window.gameLink); }; \ No newline at end of file diff --git a/src/riotTags/debugger/debugger-toolbar.tag b/src/riotTags/debugger/debugger-toolbar.tag index 99128e8a1..7d0c28b3b 100644 --- a/src/riotTags/debugger/debugger-toolbar.tag +++ b/src/riotTags/debugger/debugger-toolbar.tag @@ -49,7 +49,10 @@ debugger-toolbar setTimeout(() => { const win = nw.Window.get(); // Make sure the window does not distort due to inconsistencies in title size on different OS - win.resizeTo(Math.round(480 * (1 + win.zoomLevel * 0.2)), Math.round(40 * (1 + win.zoomLevel * 0.2))); + win.resizeTo( + Math.round(480 * (1 + win.zoomLevel * 0.2)), + Math.round(40 * (1 + win.zoomLevel * 0.2)) + ); }, 0); const menu = new nw.Menu(); @@ -61,13 +64,15 @@ debugger-toolbar })).forEach(entry => menu.append(new nw.MenuItem(entry))); // Get displays to position everything nicely - // Firstly aim for the built-in monitor (usually a keyboard is near it), then try the second one, then try the first one + // Firstly aim for the built-in monitor (usually a keyboard is near it), + // then try the second one, then try the first one + // eslint-disable-next-line new-cap nw.Screen.Init(); const {screens} = nw.Screen; const builtIn = screens.find(screen => screen.isBuiltIn); const targetScreen = builtIn || screens[1] || screens[0]; - nw.Window.open(window.gameLink, { + nw.Window.open(window.gameLink, { title: 'ct.js', icon: 'ct_ide.png', x: targetScreen.work_area.x, @@ -87,7 +92,7 @@ debugger-toolbar this.previewWindow.showDevTools(); this.previewWindow.on('devtools-closed', () => { isDevToolsOpen = false; - }) + }); }); /* Helper methods for buttons */ @@ -99,7 +104,7 @@ debugger-toolbar }; /* Buttons' event listeners */ - this.togglePause = e => { + this.togglePause = () => { // hidden negation is semantically hidden here this.gamePaused = this.previewWindow.window.PIXI.Ticker.shared.started; if (this.gamePaused) { @@ -108,13 +113,13 @@ debugger-toolbar this.previewWindow.window.PIXI.Ticker.shared.start(); } }; - this.restartGame = e => { + this.restartGame = () => { this.previewWindow.eval(null, 'window.location.reload();'); }; - this.restartRoom = e => { + this.restartRoom = () => { this.previewWindow.eval(null, 'ct.rooms.switch(ct.room.name);'); }; - this.toggleDevTools = e => { + this.toggleDevTools = () => { if (isDevToolsOpen) { this.previewWindow.eval(null, 'nw.Window.get().closeDevTools()'); } else { @@ -122,7 +127,7 @@ debugger-toolbar isDevToolsOpen = true; } }; - this.makeScreenshot = e => { + this.makeScreenshot = () => { // Ask for game canvas geometry const rect = this.previewWindow.window.document.querySelector('#ct canvas').getBoundingClientRect(); this.previewWindow.captureScreenshot({ @@ -141,14 +146,21 @@ debugger-toolbar const fs = require('fs-extra'), path = require('path'); const now = new Date(), - timestring = `${now.getFullYear()}-${('0'+(now.getMonth()+1)).slice(-2)}-${('0'+now.getDate()).slice(-2)} ${(new Date()).getHours()}-${('0'+now.getMinutes()).slice(-2)}-${('0'+now.getSeconds()).slice(-2)}`, + year = now.getFullYear(), + month = ('0' + (now.getMonth() + 1)).slice(-2), + day = ('0' + now.getDate()).slice(-2), + hours = now.getHours(), + minutes = ('0' + now.getMinutes()).slice(-2), + seconds = ('0' + now.getSeconds()).slice(-2), + timestring = `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`, name = `Screenshot of ${window.gameName || 'ct.js game'} at ${timestring}.png`, fullPath = path.join(__dirname, name); - const data = base64.replace(/^data:image\/\w+;base64,/, ''); - const buff = new Buffer(data, 'base64'); + const shotBase64 = base64.replace(/^data:image\/\w+;base64,/, ''); + const buff = new Buffer(shotBase64, 'base64'); const stream = fs.createWriteStream(fullPath); stream.on('finish', () => { - soundbox.play('Success'); + window.soundbox.play('Success'); + // eslint-disable-next-line no-new new Notification('Done!', { body: `Saved to ${fullPath} 👌`, icon: 'ct_ide.png' @@ -161,15 +173,15 @@ debugger-toolbar stream.end(buff); }); }; - this.toggleFullscreen = e => { + this.toggleFullscreen = () => { this.gameFullscreen = !this.gameFullscreen; if (this.gameFullscreen) { - this.previewWindow.enterFullscreen() + this.previewWindow.enterFullscreen(); } else { this.previewWindow.leaveFullscreen(); } }; - this.openQrCodes = e => { + this.openQrCodes = () => { if (this.qrCodesWindow) { this.qrCodesWindow.focus(); } else { @@ -188,11 +200,11 @@ debugger-toolbar }); } }; - this.openExternal = e => { + this.openExternal = () => { nw.Shell.openExternal(window.gameLink); }; - this.closeItself = e => { + this.closeItself = () => { this.previewWindow.close(true); nw.Window.get().close(); }; diff --git a/src/riotTags/export-panel.tag b/src/riotTags/export-panel.tag index 51d68e6d4..901a1c0a1 100644 --- a/src/riotTags/export-panel.tag +++ b/src/riotTags/export-panel.tag @@ -42,7 +42,7 @@ export-panel div(each="{text in log}") {text.toString()} .flexfix-footer .flexrow - button(onclick="{close}") {voc.hide} + button(onclick="{closeExporter}") {voc.hide} button(onclick="{export}") span.inlineblock.rotateccw(if="{working}") svg.feather @@ -59,11 +59,30 @@ export-panel this.log = []; global.currentProject.settings.export = global.currentProject.settings.export || {}; - this.close = function () { + this.closeExporter = function closeExporter() { this.parent.showExporter = false; this.parent.update(); }; - this.export = async e => { + const bakeIcons = async exportDir => { + const path = require('path'), + fs = require('fs-extra'); + + const png2icons = require('png2icons'), + {getTextureOrig} = require('./data/node_requires/resources/textures'); + const iconPath = getTextureOrig(global.currentProject.settings.branding.icon || -1, true), + icon = await fs.readFile(iconPath); + await fs.outputFile(path.join(exportDir, 'icon.icns'), png2icons.createICNS( + icon, + global.currentProject.settings.pixelatedrender ? png2icons.BILINEAR : png2icons.HERMITE + )); + await fs.outputFile(path.join(exportDir, 'icon.ico'), png2icons.createICO( + icon, + global.currentProject.settings.antialias ? png2icons.BILINEAR : png2icons.HERMITE, + 0, true, true + )); + }; + // eslint-disable-next-line max-lines-per-function + this.export = async () => { if (this.working) { return; } @@ -99,7 +118,8 @@ export-panel packageJson.name = global.currentProject.settings.title; packageJson.window.title = global.currentProject.settings.title; } - const startingRoom = global.currentProject.rooms.find(room => room.uid === global.currentProject.startroom) || global.currentProject.rooms[0]; + const startingRoom = global.currentProject.rooms.find(room => + room.uid === global.currentProject.startroom) || global.currentProject.rooms[0]; packageJson.window.width = startingRoom.width; packageJson.window.height = startingRoom.height; packageJson.window.mode = global.currentProject.settings.desktopMode || 'maximized'; @@ -107,22 +127,10 @@ export-panel this.log.push('Baking icons…'); this.update(); - const png2icons = require('png2icons'), - {getTextureOrig} = require('./data/node_requires/resources/textures'); - const iconPath = getTextureOrig(global.currentProject.settings.branding.icon || -1, true), - icon = await fs.readFile(iconPath); - await fs.outputFile(path.join(exportDir, 'icon.icns'), png2icons.createICNS( - icon, - global.currentProject.settings.pixelatedrender? png2icons.BILINEAR : png2icons.HERMITE - )); - await fs.outputFile(path.join(exportDir, 'icon.ico'), png2icons.createICO( - icon, - global.currentProject.settings.antialias? png2icons.BILINEAR : png2icons.HERMITE, - 0, true, true - )); - + await bakeIcons(exportDir); this.log.push('Ready to bake packages. Be patient!'); this.update(); + const packager = require('electron-packager'); const baseOptions = { // Build parameters @@ -148,40 +156,36 @@ export-panel // wtf and why do I need it? @see https://github.com/electron/electron-packager/issues/875 process.noAsar = true; + // eslint-disable-next-line no-console console.info('Messages "Packaging app for platform *" are not errors, this is how electron-packer works ¯\\_(ツ)_/¯'); - if (global.currentProject.settings.export.linux) { - this.log.push('Building for Linux…'); - const paths = await packager(Object.assign({}, baseOptions, { - platform: 'linux', - arch: 'all' - })); - this.log.push(`Linux builds are ready at these paths:\n ${paths.join('\n ')}`); - this.update(); - } - if (global.currentProject.settings.export.mac && process.platform !== 'win32') { - this.log.push('Building for MacOS…'); - this.update(); - const paths = await packager(Object.assign({}, baseOptions, { - platform: 'darwin', - arch: 'all' - })); - this.log.push(`Mac builds are ready at these paths:\n ${paths.join('\n ')}`); - this.update(); - } - if (global.currentProject.settings.export.windows) { - this.log.push('Building for Windows…'); + + const platformMap = { + linux: 'linux', + mac: 'darwin', + windows: 'win32' + }; + for (const settingKey in platformMap) { + if (!global.currentProject.settings.export[settingKey]) { + continue; + } + const platform = platformMap[settingKey]; + this.log.push(`Building for ${settingKey}…`); + // eslint-disable-next-line no-await-in-loop const paths = await packager(Object.assign({}, baseOptions, { - platform: 'win32', + platform, arch: 'all' })); - this.log.push(`Windows builds are ready at these paths:\n ${paths.join('\n ')}`); + this.log.push(`${settingKey} builds are ready at these paths:\n ${paths.join('\n ')}`); this.update(); } - this.log.push('Success!'); + + this.log.push('Success! Exported to:'); this.log.push(buildDir); alertify.success(`Success! Exported to ${buildDir}`); + this.working = false; this.update(); + nw.Shell.openItem(buildDir); } catch (e) { this.log.push(e); @@ -192,6 +196,6 @@ export-panel } }; - this.copyLog = e => { + this.copyLog = () => { nw.Clipboard.get().set(this.log.join('\n'), 'text'); }; \ No newline at end of file diff --git a/src/riotTags/font-editor.tag b/src/riotTags/font-editor.tag index debd2020e..4e164c65e 100644 --- a/src/riotTags/font-editor.tag +++ b/src/riotTags/font-editor.tag @@ -28,7 +28,7 @@ font-editor.panel.view this.mixin(window.riotWired); this.fontobj = this.opts.fontobj; this.oldTypefaceName = this.fontobj.typefaceName; - this.fontSave = e => { + this.fontSave = () => { this.parent.editingFont = false; this.parent.update(); this.parent.loadFonts(); @@ -38,7 +38,7 @@ font-editor.panel.view if (font.family === 'CTPROJFONT' + this.oldTypefaceName) { this.oldTypefaceName = this.fontobj.typefaceName; font.family = this.fontobj.typefaceName; - font.style = this.fontobj.italic? 'italic': 'normal'; + font.style = this.fontobj.italic ? 'italic' : 'normal'; font.weight = this.fontobj.weight; this.parent.loadFonts(); break; diff --git a/src/riotTags/fonts-panel.tag b/src/riotTags/fonts-panel.tag index 679fac3c8..0ac90bd03 100644 --- a/src/riotTags/fonts-panel.tag +++ b/src/riotTags/fonts-panel.tag @@ -40,9 +40,9 @@ fonts-panel.flexfix.tall.fifty path = require('path'); this.thumbnails = font => `file://${window.global.projdir}/fonts/${font.origname}_prev.png?cache=${font.lastmod}`; - this.names = font => `${font.typefaceName} ${font.weight} ${font.italic? this.voc.italic : ''}`; + this.names = font => `${font.typefaceName} ${font.weight} ${font.italic ? this.voc.italic : ''}`; - this.setUpPanel = e => { + this.setUpPanel = () => { global.currentProject.fonts = global.currentProject.fonts || []; this.fonts = global.currentProject.fonts; this.editingFont = false; @@ -56,7 +56,7 @@ fonts-panel.flexfix.tall.fifty window.signals.off('projectLoaded', this.setUpPanel); }); - this.openFont = font => e => { + this.openFont = font => () => { this.editingFont = true; this.editedFont = font; }; @@ -65,13 +65,13 @@ fonts-panel.flexfix.tall.fifty this.fontMenu = { items: [{ label: window.languageJSON.common.open, - click: e => { + click: () => { this.editingFont = true; this.update(); } }, { - label: languageJSON.common.copyName, - click: e => { + label: window.languageJSON.common.copyName, + click: () => { nw.Clipboard.get().set(this.editedFont.name, 'text'); } }, { @@ -96,7 +96,7 @@ fonts-panel.flexfix.tall.fifty alertify .okBtn(window.languageJSON.common.delete) .cancelBtn(window.languageJSON.common.cancel) - .confirm(window.languageJSON.common.confirmDelete.replace('{0}', `${this.editedFont.typefaceName} ${this.editedFont.weight} ${this.editedFont.italic? voc.italic : ''}`)) + .confirm(window.languageJSON.common.confirmDelete.replace('{0}', `${this.editedFont.typefaceName} ${this.editedFont.weight} ${this.editedFont.italic ? this.voc.italic : ''}`)) .then(e => { if (e.buttonClicked === 'ok') { const ind = global.currentProject.fonts.indexOf(this.editedFont); @@ -126,7 +126,7 @@ fonts-panel.flexfix.tall.fifty e.target.value = ''; for (let i = 0; i < files.length; i++) { if (/\.ttf/gi.test(files[i])) { - let id = generateGUID(); + const id = generateGUID(); this.loadFont( id, files[i], @@ -134,43 +134,46 @@ fonts-panel.flexfix.tall.fifty true ); } else { - // ¯\_(ツ)_/¯ + alertify.log(`Skipped ${files[i]} as it is not a .ttf file.`); + void 0; } } this.dropping = false; - e.srcElement.value = ""; // clear input value that prevent to upload the same filename again + e.srcElement.value = ''; // clear input value that prevent to upload the same filename again e.preventDefault(); }; - this.loadFont = (uid, filename, dest, imprt) => { + this.loadFont = (uid, filename, dest) => { fs.copy(filename, dest, e => { - if (e) throw e; + if (e) { + throw e; + } var obj = { typefaceName: path.basename(filename).replace('.ttf', ''), weight: 400, italic: false, - uid: uid, origname: path.basename(dest), - lastmod: +(new Date()) + lastmod: Number(new Date()), + uid }; global.currentProject.fonts.push(obj); setTimeout(() => { this.fontGenPreview(dest, dest + '_prev.png', 64, obj) - .then(dataUrl => { + .then(() => { this.refs.fonts.updateList(); this.update(); }); - }, 250) + }, 250); }); }; - this.fontGenPreview = (source, destFile, size, obj) => new Promise ((resolve, reject) => { - var template = { + this.fontGenPreview = (source, destFile, size, obj) => new Promise((resolve, reject) => { + const template = { weight: obj.weight, - style: obj.italic? 'italic' : 'normal' + style: obj.italic ? 'italic' : 'normal' }; // we clean the source url from the possible space and the \ to / (windows specific) - var cleanedSource = source.replace(/ /g, '%20').replace(/\\/g, '/'); - var face = new FontFace('CTPROJFONT' + obj.typefaceName, `url(file://${cleanedSource})`, template); - var elt = document.createElement('span'); + const cleanedSource = source.replace(/ /g, '%20').replace(/\\/g, '/'); + const face = new FontFace('CTPROJFONT' + obj.typefaceName, `url(file://${cleanedSource})`, template); + const elt = document.createElement('span'); elt.innerHTML = 'testString'; elt.style.fontFamily = obj.typefaceName; document.body.appendChild(elt); @@ -179,19 +182,18 @@ fonts-panel.flexfix.tall.fifty loaded.external = true; loaded.ctId = face.ctId = obj.uid; document.fonts.add(loaded); - var c = document.createElement('canvas'), - w, h; + const c = document.createElement('canvas'); c.x = c.getContext('2d'); c.width = c.height = size; c.x.clearRect(0, 0, size, size); - c.x.font = `${obj.italic? 'italic ' : ''}${obj.weight} ${Math.floor(size * 0.75)}px "${loaded.family}"`; + c.x.font = `${obj.italic ? 'italic ' : ''}${obj.weight} ${Math.floor(size * 0.75)}px "${loaded.family}"`; c.x.fillStyle = '#000'; c.x.fillText('Aa', size * 0.05, size * 0.75); // strip off the data:image url prefix to get just the base64-encoded bytes - var dataURL = c.toDataURL(); - var data = dataURL.replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); - var stream = fs.createWriteStream(destFile); + const dataURL = c.toDataURL(); + const previewBuffer = dataURL.replace(/^data:image\/\w+;base64,/, ''); + const buf = new Buffer(previewBuffer, 'base64'); + const stream = fs.createWriteStream(destFile); stream.on('finish', () => { setTimeout(() => { // WHY THE HECK I EVER NEED THIS?! resolve(destFile); @@ -210,7 +212,7 @@ fonts-panel.flexfix.tall.fifty var dragTimer; this.onDragOver = e => { var dt = e.dataTransfer; - if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('Files'))) { + if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) { this.dropping = true; this.update(); window.clearTimeout(dragTimer); @@ -219,12 +221,12 @@ fonts-panel.flexfix.tall.fifty e.stopPropagation(); }; this.onDrop = e => { - e.stopPropagation(); + e.stopPropagation(); }; this.onDragLeave = e => { dragTimer = window.setTimeout(() => { this.dropping = false; - this.update() + this.update(); }, 25); e.preventDefault(); e.stopPropagation(); @@ -241,20 +243,20 @@ fonts-panel.flexfix.tall.fifty }); this.loadFonts = () => { - var fonts = global.currentProject.fonts; + const {fonts} = global.currentProject; for (const font of document.fonts) { if (font.external) { document.fonts.delete(font); } } for (const font of fonts) { - var template = { - weight: font.weight, - style: font.italic? 'italic' : 'normal' - }, - source = `${global.projdir}/fonts/${font.origname}`; - var cleanedSource = source.replace(/ /g, '%20').replace(/\\/g, '/'); - var face = new FontFace('CTPROJFONT' + font.typefaceName, `url(file://${cleanedSource})`, template); + const template = { + weight: font.weight, + style: font.italic ? 'italic' : 'normal' + }; + const source = `${global.projdir}/fonts/${font.origname}`, + cleanedSource = source.replace(/ /g, '%20').replace(/\\/g, '/'); + const face = new FontFace('CTPROJFONT' + font.typefaceName, `url(file://${cleanedSource})`, template); face.load() .then(loaded => { loaded.external = true; diff --git a/src/riotTags/license-panel.tag b/src/riotTags/license-panel.tag index 7b1ff7d54..5d011f061 100644 --- a/src/riotTags/license-panel.tag +++ b/src/riotTags/license-panel.tag @@ -654,7 +654,7 @@ license-panel.modal.pad script. this.namespace = 'licensepanel'; this.mixin(window.riotVoc); - this.closeModal = e => { + this.closeModal = () => { this.parent.showLicense = false; this.parent.update(); }; diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index f068820fd..4ece0fa42 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -1,588 +1,570 @@ -main-menu.flexcol - nav.nogrow.flexrow(if="{global.currentProject}") - ul#fullscreen.nav - li.nbr(onclick="{toggleFullscreen}" title="{voc.min} (F11)") - svg.feather - use(xlink:href="data/icons.svg#{fullscreen? 'minimize-2' : 'maximize-2'}" data-hotkey="F11") - - ul#app.nav.tabs - li.it30#ctlogo(onclick="{ctClick}" title="{voc.ctIDE}") - svg.feather.nmr - use(xlink:href="data/icons.svg#menu") - context-menu#theCatMenu(menu="{catMenu}" ref="catMenu") - li.it30(onclick="{changeTab('patrons')}" title="{voc.patrons}" class="{active: tab === 'patrons'}") - svg.feather - use(xlink:href="data/icons.svg#heart") - li.it30(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") - svg.feather - use(xlink:href="data/icons.svg#save") - li.nbr.it30(onclick="{runProject}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") - svg.feather - use(xlink:href="data/icons.svg#play") - - ul#mainnav.nav.tabs - li(onclick="{changeTab('settings')}" class="{active: tab === 'settings'}" data-hotkey="Control+1" title="Control+1") - svg.feather - use(xlink:href="data/icons.svg#settings") - span {voc.settings} - li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}" data-hotkey="Control+2" title="Control+2") - svg.feather - use(xlink:href="data/icons.svg#ctmod") - span {voc.modules} - li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}" data-hotkey="Control+3" title="Control+3") - svg.feather - use(xlink:href="data/icons.svg#coin") - span {voc.texture} - li(onclick="{changeTab('ui')}" class="{active: tab === 'ui'}" data-hotkey="Control+4" title="Control+4") - svg.feather - use(xlink:href="data/icons.svg#droplet") - span {voc.ui} - li(onclick="{changeTab('fx')}" class="{active: tab === 'fx'}" data-hotkey="Control+5" title="Control+5") - svg.feather - use(xlink:href="data/icons.svg#sparkles") - span {voc.fx} - li(onclick="{changeTab('sounds')}" class="{active: tab === 'sounds'}" data-hotkey="Control+6" title="Control+6") - svg.feather - use(xlink:href="data/icons.svg#headphones") - span {voc.sounds} - li(onclick="{changeTab('types')}" class="{active: tab === 'types'}" data-hotkey="Control+7" title="Control+7") - svg.feather - use(xlink:href="data/icons.svg#user") - span {voc.types} - li(onclick="{changeTab('rooms')}" class="{active: tab === 'rooms'}" data-hotkey="Control+8" title="Control+8") - svg.feather - use(xlink:href="data/icons.svg#room") - span {voc.rooms} - div.flexitem.relative(if="{global.currentProject}") - settings-panel(show="{tab === 'settings'}" data-hotkey-scope="settings") - modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") - textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") - ui-panel(show="{tab === 'ui'}" data-hotkey-scope="ui") - fx-panel(show="{tab === 'fx'}" data-hotkey-scope="fx") - sounds-panel(show="{tab === 'sounds'}" data-hotkey-scope="sounds") - types-panel(show="{tab === 'types'}" data-hotkey-scope="types") - rooms-panel(show="{tab === 'rooms'}" data-hotkey-scope="rooms") - license-panel(if="{showLicense}") - patreon-screen(if="{tab === 'patrons'}" data-hotkey-scope="patrons") - export-panel(show="{showExporter}") - new-project-onboarding(if="{sessionStorage.showOnboarding && localStorage.showOnboarding !== 'off'}") - script. - const fs = require('fs-extra'), - path = require('path'); - const archiver = require('archiver'); - const glob = require('./data/node_requires/glob'); - - // Mounts the hotkey plugins, enabling hotkeys on elements with data-hotkey attributes - const hotkey = require('./data/node_requires/hotkeys')(document); - this.on('unmount', () => { - hotkey.unmount(); - }); - - this.namespace = 'menu'; - this.mixin(window.riotVoc); - - this.tab = 'settings'; - this.changeTab = tab => e => { - this.tab = tab; - hotkey.cleanScope(); - hotkey.push(tab); - window.signals.trigger('globalTabChanged'); - window.signals.trigger(`${tab}Focus`); - }; - - this.fullscreen = false; - this.toggleFullscreen = function() { - this.fullscreen = !this.fullscreen; - if (this.fullscreen) { - nw.Window.get().enterFullscreen(); - } else { - nw.Window.get().leaveFullscreen(); - } - }; - - const languageSubmenu = { - items: [], - columns: 2 - }; - const recentProjectsSubmenu = { - items: [] - }; - this.refreshLatestProject = function() { - recentProjectsSubmenu.items.length = 0; - var lastProjects; - if (('lastProjects' in localStorage) && - (localStorage.lastProjects !== '')) { - lastProjects = localStorage.lastProjects.split(';'); - } else { - lastProjects = []; - } - for (const project of lastProjects) { - recentProjectsSubmenu.items.push({ - label: project, - click: function () { - if (!confirm(window.languageJSON.common.reallyexit)) { - return false; - } - window.signals.trigger('resetAll'); - window.loadProject(project); - } - }); - } - }; - this.ctClick = (e) => { - this.refreshLatestProject(); - if (e) { - this.refs.catMenu.toggle(); - } - }; - this.saveProject = () => { - const YAML = require('js-yaml'); - const data = YAML.safeDump(global.currentProject); - return fs.outputFile(global.projdir + '.ict', data) - .then(() => { - alertify.success(languageJSON.common.savedcomm, "success", 3000); - this.saveRecoveryDebounce(); - fs.remove(global.projdir + '.ict.recovery') - .then(() => console.log()) - .catch(console.error); - glob.modified = false; - }) - .catch(alertify.error); - }; - this.saveRecovery = () => { - if (global.currentProject) { - const YAML = require('js-yaml'); - const data = YAML.safeDump(global.currentProject); - fs.outputFile(global.projdir + '.ict.recovery', data); - } - this.saveRecoveryDebounce(); - }; - this.saveRecoveryDebounce = debounce(this.saveRecovery, 1000 * 60 * 5); - window.signals.on('saveProject', this.saveProject); - this.on('unmount', () => { - window.signals.off('saveProject', this.saveProject); - }); - this.saveRecoveryDebounce(); - - const {getWritableDir} = require('./data/node_requires/platformUtils'); - // Run a local server for ct.js games - let fileServer; - getWritableDir().then(dir => { - const nstatic = require('node-static'); - fileServer = new nstatic.Server(path.join(dir, '/export/'), { - cache: false, - serverInfo: 'ctjsgameeditor' - }); - console.log('[serverPath]', path.join(dir, '/export/')); - }); - const server = require('http').createServer(function (request, response) { - request.addListener('end', function () { - fileServer.serve(request, response); - }).resume(); - }); - server.listen(0); - - this.runProject = e => { - document.body.style.cursor = 'progress'; - const runCtExport = require('./data/node_requires/exporter'); - runCtExport(global.currentProject, global.projdir) - .then(path => { - if (localStorage.disableBuiltInDebugger === 'yes') { - nw.Shell.openExternal(`http://localhost:${server.address().port}/`); - } else { - window.openDebugger(`http://localhost:${server.address().port}`); - } - }) - .catch(e => { - window.alertify.error(e); - console.error(e); - }) - .finally(() => { - document.body.style.cursor = ''; - }); - }; - this.runProjectAlt = e => { - const runCtExport = require('./data/node_requires/exporter'); - runCtExport(global.currentProject, global.projdir) - .then(path => { - console.log(path); - nw.Shell.openExternal(`http://localhost:${server.address().port}/`); - }); - }; - hotkey.on('Alt+F5', this.runProjectAlt); - - this.zipProject = async e => { - try { - const os = require('os'); - const path = require('path'); - - const writable = await getWritableDir(); - const inDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ctZipProject-')), - outName = path.join(writable, `/${sessionStorage.projname}.zip`); - - await this.saveProject(); - await fs.remove(outName); - await fs.remove(inDir); - await fs.copy(global.projdir + '.ict', path.join(inDir, sessionStorage.projname)); - await fs.copy(global.projdir, path.join(inDir, sessionStorage.projname.slice(0, -4))); - - const archive = archiver('zip'), - output = fs.createWriteStream(outName); - - output.on('close', () => { - nw.Shell.showItemInFolder(outName); - alertify.success(this.voc.successZipProject.replace('{0}', outName)); - fs.remove(inDir); - }); - - archive.pipe(output); - archive.directory(inDir, false); - archive.finalize(); - } catch (e) { - alertify.error(e); - } - }; - this.zipExport = async e => { - const writable = await getWritableDir(); - const runCtExport = require('./data/node_requires/exporter'); - let exportFile = path.join(writable, '/export.zip'), - inDir = path.join(writable, '/export/'); - await fs.remove(exportFile); - runCtExport(global.currentProject, global.projdir) - .then(() => { - let archive = archiver('zip'), - output = fs.createWriteStream(exportFile); - - output.on('close', () => { - nw.Shell.showItemInFolder(exportFile); - alertify.success(this.voc.successZipExport.replace('{0}', exportFile)); - }); - - archive.pipe(output); - archive.directory(inDir, false); - archive.finalize(); - }) - .catch(alertify.error); - }; - localStorage.UItheme = localStorage.UItheme || 'Day'; - this.switchTheme = theme => { - localStorage.UItheme = theme; - document.getElementById('themeCSS').href = `./data/theme${theme}.css`; - window.signals.trigger('UIThemeChanged', theme); - }; - - const troubleshootingSubmenu = { - items: [{ - label: window.languageJSON.menu.toggleDevTools, - icon: 'terminal', - hotkeyLabel: 'Ctrl+Shift+C', - click: () => { - const win = nw.Window.get(); - win.showDevTools(); - } - }, { - label: window.languageJSON.menu.copySystemInfo, - icon: 'file-text', - click: async () => { - const os = require('os'), - path = require('path'); - const YAML = require('js-yaml'); - const packaged = path.basename(process.execPath, path.extname(process.execPath)) !== 'nw'; - const report = `Ct.js v${process.versions.ctjs} 😽 ${packaged? '(packaged)' : '(runs from sources)'}\n\n` + - `NW.JS v${process.versions.nw}\n` + - `Chromium v${process.versions.chromium}\n` + - `Node.js v${process.versions.node}\n` + - `Pixi.js v${PIXI.VERSION}\n\n` + - `OS ${process.platform} ${process.arch} // ${os.type()} ${os.release()}`; - nw.Clipboard.get().set(report, 'text'); - } - }, /*{ - label: window.languageJSON.menu.disableAcceleration, - type: 'checkbox', - checked: () => fs.existsSync('./pleaseCtJSLoadWithoutGPUAccelerationMmkay'), - click: async () => { - if (await fs.exists('./pleaseCtJSLoadWithoutGPUAccelerationMmkay')) { - await fs.remove('./pleaseCtJSLoadWithoutGPUAccelerationMmkay'); - } else { - await fs.outputFile('./pleaseCtJSLoadWithoutGPUAccelerationMmkay', 'Do it.'); - } - this.update(); - } - },*/ { - label: window.languageJSON.menu.disableBuiltInDebugger, - type: 'checkbox', - checked: () => localStorage.disableBuiltInDebugger === 'yes', - click: () => { - if (localStorage.disableBuiltInDebugger === 'yes') { - localStorage.disableBuiltInDebugger = 'no'; - } else { - localStorage.disableBuiltInDebugger = 'yes'; - } - } - }, { - type: 'separator' - }, { - icon: 'discord', - iconClass: 'icon', - label: window.languageJSON.menu.visitDiscordForGamedevSupport, - click: () => { - nw.Shell.openExternal('https://discord.gg/3f7TsRC'); - } - }, { - icon: 'github', - iconClass: 'icon', - label: window.languageJSON.menu.postAnIssue, - click: () => { - nw.Shell.openExternal('https://github.com/ct-js/ct-js/issues/new/choose'); - } - }] - }; - - const settingsSubmenu = { - items: [{ - label: window.languageJSON.common.language, - submenu: languageSubmenu - }, { - label: window.languageJSON.menu.theme, - submenu: { - items: [{ - label: window.languageJSON.menu.themeDay, - icon: () => localStorage.UItheme === 'Day' && 'check', - click: () => { - this.switchTheme('Day'); - } - }, { - label: window.languageJSON.menu.themeNight, - icon: () => localStorage.UItheme === 'Night' && 'check', - click: () => { - this.switchTheme('Night'); - } - }, { - label: window.languageJSON.menu.themeHorizon || 'Horizon', - icon: () => localStorage.UItheme === 'Horizon' && 'check', - click: () => { - this.switchTheme('Horizon'); - } - }] - } - }, { - label: window.languageJSON.menu.codeFont, - submenu: { - items: [{ - label: window.languageJSON.menu.codeFontDefault, - icon: () => !localStorage.fontFamily && 'check', - click: () => { - localStorage.fontFamily = ''; - window.signals.trigger('codeFontUpdated'); - } - }, { - label: window.languageJSON.menu.codeFontOldSchool, - icon: () => localStorage.fontFamily === 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace' && 'check', - click: () => { - localStorage.fontFamily = 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace'; - window.signals.trigger('codeFontUpdated'); - } - }, { - label: window.languageJSON.menu.codeFontSystem, - icon: () => localStorage.fontFamily === 'monospace' && 'check', - click: () => { - localStorage.fontFamily = 'monospace'; - window.signals.trigger('codeFontUpdated'); - } - }, { - label: window.languageJSON.menu.codeFontCustom, - click: () => { - alertify - .defaultValue(localStorage.fontFamily || '') - .prompt(window.languageJSON.menu.newFont) - .then(e => { - if (e.inputValue && e.buttonClicked !== 'cancel') { - localStorage.fontFamily = `"${e.inputValue}", monospace`; - } - window.signals.trigger('codeFontUpdated'); - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.menu.codeLigatures, - type: 'checkbox', - checked: () => localStorage.codeLigatures !== 'off', - click: () => { - localStorage.codeLigatures = localStorage.codeLigatures === 'off'? 'on' : 'off'; - window.signals.trigger('codeFontUpdated'); - } - }, { - label: window.languageJSON.menu.codeDense, - type: 'checkbox', - checked: () => localStorage.codeDense === 'on', - click: () => { - localStorage.codeDense = localStorage.codeDense === 'off'? 'on' : 'off'; - window.signals.trigger('codeFontUpdated'); - } - }] - } - }, { - type: 'separator' - }, { - label: window.languageJSON.common.zoomIn, - icon: 'zoom-in', - click: e => { - const win = nw.Window.get(); - let zoom = win.zoomLevel + 0.5; - if (Number.isNaN(zoom) || !zoom || !Number.isFinite(zoom)) { - zoom = 0; - } else if (zoom > 5) { - zoom = 5; - } - win.zoomLevel = zoom; - - console.debug('Zoom in to ', zoom); - localStorage.editorZooming = zoom; - }, - hotkey: 'Control+=', - hotkeyLabel: 'Ctrl+=' - }, { - label: window.languageJSON.common.zoomOut, - icon: 'zoom-out', - click: e => { - const win = nw.Window.get(); - let zoom = win.zoomLevel - 0.5; - if (Number.isNaN(zoom) || !zoom || !Number.isFinite(zoom)) { - zoom = 0; - } else if (zoom < -3) { - zoom = -3; - } - win.zoomLevel = zoom; - - console.debug('Zoom out to ', zoom); - localStorage.editorZooming = zoom; - }, - hotkey: 'Control+-', - hotkeyLabel: 'Ctrl+-' - }] - }; - - this.catMenu = { - items: [{ - label: window.languageJSON.common.save, - icon: 'save', - click: this.saveProject, - hotkey: 'Control+s', - hotkeyLabel: 'Ctrl+S' - }, { - label: this.voc.exportDesktop, - click: e => { - this.showExporter = true; - this.update(); - }, - icon: 'package' - }, { - label: this.voc.zipExport, - click: this.zipExport, - icon: 'upload-cloud' - }, { - label: this.voc.zipProject, - click: this.zipProject - }, { - label: this.voc.openIncludeFolder, - click: e => { - fs.ensureDir(path.join(global.projdir, '/include')) - .then(() => { - nw.Shell.openItem(path.join(global.projdir, '/include')); - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.menu.startScreen, - click: (e) => { - if (!confirm(window.languageJSON.common.reallyexit)) { - return false; - } - window.signals.trigger('resetAll'); - } - }, { - label: window.languageJSON.intro.latest, - submenu: recentProjectsSubmenu - }, { - type: 'separator' - }, { - label: window.languageJSON.menu.settings, - submenu: settingsSubmenu, - icon: 'settings' - }, { - label: window.languageJSON.common.contribute, - click: function () { - nw.Shell.openExternal('https://github.com/ct-js/ct-js'); - }, - icon: 'code' - }, { - label: window.languageJSON.common.donate, - icon: 'heart', - click: function () { - nw.Shell.openExternal('https://www.patreon.com/comigo'); - } - }, { - label: window.languageJSON.menu.troubleshooting, - icon: 'alert-circle', - submenu: troubleshootingSubmenu - }, { - label: window.languageJSON.common.ctsite, - click: function () { - nw.Shell.openExternal('https://ctjs.rocks/'); - } - }, { - label: window.languageJSON.menu.license, - click: () => { - this.showLicense = true; - this.update(); - } - }] - }; - this.switchLanguage = filename => { - const i18n = require('./data/node_requires/i18n.js'); - const {extend} = require('./data/node_requires/objectUtils'); - try { - window.languageJSON = i18n.loadLanguage(filename); - localStorage.appLanguage = filename; - window.signals.trigger('updateLocales'); - window.riot.update(); - console.log('Applied a new language file.'); - } catch(e) { - alert('Could not open a language file: ' + e); - } - }; - var switchLanguage = this.switchLanguage; - - fs.readdir('./data/i18n/') - .then(files => { - files.forEach(filename => { - if (path.extname(filename) !== '.json') { - return; - } - var file = filename.slice(0, -5); - if (file === 'Comments') { - return; - } - languageSubmenu.items.push({ - label: file, - icon: () => localStorage.appLanguage === file && 'check', - click: function() { - switchLanguage(file); - } - }); - }); - languageSubmenu.items.push({ - type: 'separator' - }); - languageSubmenu.items.push({ - label: window.languageJSON.common.translateToYourLanguage, - click: function() { - nw.Shell.openExternal('https://github.com/ct-js/ct-js/tree/develop/app/data/i18n'); - } - }); - }) - .catch(e => { - alert('Could not get i18n files: ' + e); +main-menu.flexcol + nav.nogrow.flexrow(if="{global.currentProject}") + ul#fullscreen.nav + li.nbr(onclick="{toggleFullscreen}" title="{voc.min} (F11)") + svg.feather + use(xlink:href="data/icons.svg#{fullscreen? 'minimize-2' : 'maximize-2'}" data-hotkey="F11") + + ul#app.nav.tabs + li.it30#ctlogo(onclick="{ctClick}" title="{voc.ctIDE}") + svg.feather.nmr + use(xlink:href="data/icons.svg#menu") + context-menu#theCatMenu(menu="{catMenu}" ref="catMenu") + li.it30(onclick="{changeTab('patrons')}" title="{voc.patrons}" class="{active: tab === 'patrons'}") + svg.feather + use(xlink:href="data/icons.svg#heart") + li.it30(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") + svg.feather + use(xlink:href="data/icons.svg#save") + li.nbr.it30(onclick="{runProject}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") + svg.feather + use(xlink:href="data/icons.svg#play") + + ul#mainnav.nav.tabs + li(onclick="{changeTab('settings')}" class="{active: tab === 'settings'}" data-hotkey="Control+1" title="Control+1") + svg.feather + use(xlink:href="data/icons.svg#settings") + span {voc.settings} + li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}" data-hotkey="Control+2" title="Control+2") + svg.feather + use(xlink:href="data/icons.svg#ctmod") + span {voc.modules} + li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}" data-hotkey="Control+3" title="Control+3") + svg.feather + use(xlink:href="data/icons.svg#coin") + span {voc.texture} + li(onclick="{changeTab('ui')}" class="{active: tab === 'ui'}" data-hotkey="Control+4" title="Control+4") + svg.feather + use(xlink:href="data/icons.svg#droplet") + span {voc.ui} + li(onclick="{changeTab('fx')}" class="{active: tab === 'fx'}" data-hotkey="Control+5" title="Control+5") + svg.feather + use(xlink:href="data/icons.svg#sparkles") + span {voc.fx} + li(onclick="{changeTab('sounds')}" class="{active: tab === 'sounds'}" data-hotkey="Control+6" title="Control+6") + svg.feather + use(xlink:href="data/icons.svg#headphones") + span {voc.sounds} + li(onclick="{changeTab('types')}" class="{active: tab === 'types'}" data-hotkey="Control+7" title="Control+7") + svg.feather + use(xlink:href="data/icons.svg#user") + span {voc.types} + li(onclick="{changeTab('rooms')}" class="{active: tab === 'rooms'}" data-hotkey="Control+8" title="Control+8") + svg.feather + use(xlink:href="data/icons.svg#room") + span {voc.rooms} + div.flexitem.relative(if="{global.currentProject}") + settings-panel(show="{tab === 'settings'}" data-hotkey-scope="settings") + modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") + textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") + ui-panel(show="{tab === 'ui'}" data-hotkey-scope="ui") + fx-panel(show="{tab === 'fx'}" data-hotkey-scope="fx") + sounds-panel(show="{tab === 'sounds'}" data-hotkey-scope="sounds") + types-panel(show="{tab === 'types'}" data-hotkey-scope="types") + rooms-panel(show="{tab === 'rooms'}" data-hotkey-scope="rooms") + license-panel(if="{showLicense}") + patreon-screen(if="{tab === 'patrons'}" data-hotkey-scope="patrons") + export-panel(show="{showExporter}") + new-project-onboarding(if="{sessionStorage.showOnboarding && localStorage.showOnboarding !== 'off'}") + script. + const fs = require('fs-extra'), + path = require('path'); + const archiver = require('archiver'); + const glob = require('./data/node_requires/glob'); + + // Mounts the hotkey plugins, enabling hotkeys on elements with data-hotkey attributes + const hotkey = require('./data/node_requires/hotkeys')(document); + this.on('unmount', () => { + hotkey.unmount(); + }); + + this.namespace = 'menu'; + this.mixin(window.riotVoc); + + this.tab = 'settings'; + this.changeTab = tab => () => { + this.tab = tab; + hotkey.cleanScope(); + hotkey.push(tab); + window.signals.trigger('globalTabChanged'); + window.signals.trigger(`${tab}Focus`); + }; + + this.fullscreen = false; + this.toggleFullscreen = function toggleFullscreen() { + this.fullscreen = !this.fullscreen; + if (this.fullscreen) { + nw.Window.get().enterFullscreen(); + } else { + nw.Window.get().leaveFullscreen(); + } + }; + + const languageSubmenu = { + items: [], + columns: 2 + }; + const recentProjectsSubmenu = { + items: [] + }; + this.refreshLatestProject = function refreshLatestProject() { + recentProjectsSubmenu.items.length = 0; + var lastProjects; + if (('lastProjects' in localStorage) && + (localStorage.lastProjects !== '')) { + lastProjects = localStorage.lastProjects.split(';'); + } else { + lastProjects = []; + } + for (const project of lastProjects) { + recentProjectsSubmenu.items.push({ + label: project, + click() { + alertify.confirm(window.languageJSON.common.reallyexit, e => { + if (e) { + window.signals.trigger('resetAll'); + window.loadProject(project); + } + }); + } + }); + } + }; + this.ctClick = (e) => { + this.refreshLatestProject(); + if (e) { + this.refs.catMenu.toggle(); + } + }; + this.saveProject = () => { + const YAML = require('js-yaml'); + const projectYAML = YAML.safeDump(global.currentProject); + return fs.outputFile(global.projdir + '.ict', projectYAML) + .then(() => { + alertify.success(window.languageJSON.common.savedcomm, 'success', 3000); + this.saveRecoveryDebounce(); + fs.remove(global.projdir + '.ict.recovery') + .catch(console.error); + glob.modified = false; + }) + .catch(alertify.error); + }; + this.saveRecovery = () => { + if (global.currentProject) { + const YAML = require('js-yaml'); + const recoveryYAML = YAML.safeDump(global.currentProject); + fs.outputFile(global.projdir + '.ict.recovery', recoveryYAML); + } + this.saveRecoveryDebounce(); + }; + this.saveRecoveryDebounce = window.debounce(this.saveRecovery, 1000 * 60 * 5); + window.signals.on('saveProject', this.saveProject); + this.on('unmount', () => { + window.signals.off('saveProject', this.saveProject); + }); + this.saveRecoveryDebounce(); + + const {getWritableDir} = require('./data/node_requires/platformUtils'); + // Run a local server for ct.js games + let fileServer; + getWritableDir().then(dir => { + const nstatic = require('node-static'); + fileServer = new nstatic.Server(path.join(dir, '/export/'), { + cache: false, + serverInfo: 'ctjsgameeditor' + }); + }); + const server = require('http').createServer((request, response) => { + request.addListener('end', () => { + fileServer.serve(request, response); + }).resume(); + }); + server.listen(0); + + this.runProject = () => { + document.body.style.cursor = 'progress'; + const runCtExport = require('./data/node_requires/exporter'); + runCtExport(global.currentProject, global.projdir) + .then(() => { + if (localStorage.disableBuiltInDebugger === 'yes') { + nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + } else { + window.openDebugger(`http://localhost:${server.address().port}`); + } + }) + .catch(e => { + window.alertify.error(e); + console.error(e); + }) + .finally(() => { + document.body.style.cursor = ''; + }); + }; + this.runProjectAlt = () => { + const runCtExport = require('./data/node_requires/exporter'); + runCtExport(global.currentProject, global.projdir) + .then(() => { + nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + }); + }; + hotkey.on('Alt+F5', this.runProjectAlt); + + this.zipProject = async () => { + try { + const os = require('os'); + const path = require('path'); + + const writable = await getWritableDir(); + const inDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ctZipProject-')), + outName = path.join(writable, `/${sessionStorage.projname}.zip`); + + await this.saveProject(); + await fs.remove(outName); + await fs.remove(inDir); + await fs.copy(global.projdir + '.ict', path.join(inDir, sessionStorage.projname)); + await fs.copy(global.projdir, path.join(inDir, sessionStorage.projname.slice(0, -4))); + + const archive = archiver('zip'), + output = fs.createWriteStream(outName); + + output.on('close', () => { + nw.Shell.showItemInFolder(outName); + alertify.success(this.voc.successZipProject.replace('{0}', outName)); + fs.remove(inDir); + }); + + archive.pipe(output); + archive.directory(inDir, false); + archive.finalize(); + } catch (e) { + alertify.error(e); + } + }; + this.zipExport = async () => { + const writable = await getWritableDir(); + const runCtExport = require('./data/node_requires/exporter'); + const exportFile = path.join(writable, '/export.zip'), + inDir = path.join(writable, '/export/'); + await fs.remove(exportFile); + runCtExport(global.currentProject, global.projdir) + .then(() => { + const archive = archiver('zip'), + output = fs.createWriteStream(exportFile); + + output.on('close', () => { + nw.Shell.showItemInFolder(exportFile); + alertify.success(this.voc.successZipExport.replace('{0}', exportFile)); + }); + + archive.pipe(output); + archive.directory(inDir, false); + archive.finalize(); + }) + .catch(alertify.error); + }; + localStorage.UItheme = localStorage.UItheme || 'Day'; + this.switchTheme = theme => { + localStorage.UItheme = theme; + document.getElementById('themeCSS').href = `./data/theme${theme}.css`; + window.signals.trigger('UIThemeChanged', theme); + }; + + const troubleshootingSubmenu = { + items: [{ + label: window.languageJSON.menu.toggleDevTools, + icon: 'terminal', + hotkeyLabel: 'Ctrl+Shift+C', + click: () => { + const win = nw.Window.get(); + win.showDevTools(); + } + }, { + label: window.languageJSON.menu.copySystemInfo, + icon: 'file-text', + click: () => { + const os = require('os'), + path = require('path'); + const packaged = path.basename(process.execPath, path.extname(process.execPath)) !== 'nw'; + const report = `Ct.js v${process.versions.ctjs} 😽 ${packaged ? '(packaged)' : '(runs from sources)'}\n\n` + + `NW.JS v${process.versions.nw}\n` + + `Chromium v${process.versions.chromium}\n` + + `Node.js v${process.versions.node}\n` + + `Pixi.js v${PIXI.VERSION}\n\n` + + `OS ${process.platform} ${process.arch} // ${os.type()} ${os.release()}`; + nw.Clipboard.get().set(report, 'text'); + } + }, { + label: window.languageJSON.menu.disableBuiltInDebugger, + type: 'checkbox', + checked: () => localStorage.disableBuiltInDebugger === 'yes', + click: () => { + if (localStorage.disableBuiltInDebugger === 'yes') { + localStorage.disableBuiltInDebugger = 'no'; + } else { + localStorage.disableBuiltInDebugger = 'yes'; + } + } + }, { + type: 'separator' + }, { + icon: 'discord', + iconClass: 'icon', + label: window.languageJSON.menu.visitDiscordForGamedevSupport, + click: () => { + nw.Shell.openExternal('https://discord.gg/3f7TsRC'); + } + }, { + icon: 'github', + iconClass: 'icon', + label: window.languageJSON.menu.postAnIssue, + click: () => { + nw.Shell.openExternal('https://github.com/ct-js/ct-js/issues/new/choose'); + } + }] + }; + + const settingsSubmenu = { + items: [{ + label: window.languageJSON.common.language, + submenu: languageSubmenu + }, { + label: window.languageJSON.menu.theme, + submenu: { + items: [{ + label: window.languageJSON.menu.themeDay, + icon: () => localStorage.UItheme === 'Day' && 'check', + click: () => { + this.switchTheme('Day'); + } + }, { + label: window.languageJSON.menu.themeNight, + icon: () => localStorage.UItheme === 'Night' && 'check', + click: () => { + this.switchTheme('Night'); + } + }, { + label: window.languageJSON.menu.themeHorizon || 'Horizon', + icon: () => localStorage.UItheme === 'Horizon' && 'check', + click: () => { + this.switchTheme('Horizon'); + } + }] + } + }, { + label: window.languageJSON.menu.codeFont, + submenu: { + items: [{ + label: window.languageJSON.menu.codeFontDefault, + icon: () => !localStorage.fontFamily && 'check', + click: () => { + localStorage.fontFamily = ''; + window.signals.trigger('codeFontUpdated'); + } + }, { + label: window.languageJSON.menu.codeFontOldSchool, + icon: () => localStorage.fontFamily === 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace' && 'check', + click: () => { + localStorage.fontFamily = 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace'; + window.signals.trigger('codeFontUpdated'); + } + }, { + label: window.languageJSON.menu.codeFontSystem, + icon: () => localStorage.fontFamily === 'monospace' && 'check', + click: () => { + localStorage.fontFamily = 'monospace'; + window.signals.trigger('codeFontUpdated'); + } + }, { + label: window.languageJSON.menu.codeFontCustom, + click: () => { + alertify + .defaultValue(localStorage.fontFamily || '') + .prompt(window.languageJSON.menu.newFont) + .then(e => { + if (e.inputValue && e.buttonClicked !== 'cancel') { + localStorage.fontFamily = `"${e.inputValue}", monospace`; + } + window.signals.trigger('codeFontUpdated'); + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.menu.codeLigatures, + type: 'checkbox', + checked: () => localStorage.codeLigatures !== 'off', + click: () => { + localStorage.codeLigatures = localStorage.codeLigatures === 'off' ? 'on' : 'off'; + window.signals.trigger('codeFontUpdated'); + } + }, { + label: window.languageJSON.menu.codeDense, + type: 'checkbox', + checked: () => localStorage.codeDense === 'on', + click: () => { + localStorage.codeDense = localStorage.codeDense === 'off' ? 'on' : 'off'; + window.signals.trigger('codeFontUpdated'); + } + }] + } + }, { + type: 'separator' + }, { + label: window.languageJSON.common.zoomIn, + icon: 'zoom-in', + click: () => { + const win = nw.Window.get(); + let zoom = win.zoomLevel + 0.5; + if (Number.isNaN(zoom) || !zoom || !Number.isFinite(zoom)) { + zoom = 0; + } else if (zoom > 5) { + zoom = 5; + } + win.zoomLevel = zoom; + + localStorage.editorZooming = zoom; + }, + hotkey: 'Control+=', + hotkeyLabel: 'Ctrl+=' + }, { + label: window.languageJSON.common.zoomOut, + icon: 'zoom-out', + click: () => { + const win = nw.Window.get(); + let zoom = win.zoomLevel - 0.5; + if (Number.isNaN(zoom) || !zoom || !Number.isFinite(zoom)) { + zoom = 0; + } else if (zoom < -3) { + zoom = -3; + } + win.zoomLevel = zoom; + + localStorage.editorZooming = zoom; + }, + hotkey: 'Control+-', + hotkeyLabel: 'Ctrl+-' + }] + }; + + this.catMenu = { + items: [{ + label: window.languageJSON.common.save, + icon: 'save', + click: this.saveProject, + hotkey: 'Control+s', + hotkeyLabel: 'Ctrl+S' + }, { + label: this.voc.exportDesktop, + click: () => { + this.showExporter = true; + this.update(); + }, + icon: 'package' + }, { + label: this.voc.zipExport, + click: this.zipExport, + icon: 'upload-cloud' + }, { + label: this.voc.zipProject, + click: this.zipProject + }, { + label: this.voc.openIncludeFolder, + click: () => { + fs.ensureDir(path.join(global.projdir, '/include')) + .then(() => { + nw.Shell.openItem(path.join(global.projdir, '/include')); + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.menu.startScreen, + click: () => { + alertify.confirm(window.languageJSON.common.reallyexit, e => { + if (e) { + window.signals.trigger('resetAll'); + } + }); + } + }, { + label: window.languageJSON.intro.latest, + submenu: recentProjectsSubmenu + }, { + type: 'separator' + }, { + label: window.languageJSON.menu.settings, + submenu: settingsSubmenu, + icon: 'settings' + }, { + label: window.languageJSON.common.contribute, + click: () => { + nw.Shell.openExternal('https://github.com/ct-js/ct-js'); + }, + icon: 'code' + }, { + label: window.languageJSON.common.donate, + icon: 'heart', + click: () => { + nw.Shell.openExternal('https://www.patreon.com/comigo'); + } + }, { + label: window.languageJSON.menu.troubleshooting, + icon: 'alert-circle', + submenu: troubleshootingSubmenu + }, { + label: window.languageJSON.common.ctsite, + click: () => { + nw.Shell.openExternal('https://ctjs.rocks/'); + } + }, { + label: window.languageJSON.menu.license, + click: () => { + this.showLicense = true; + this.update(); + } + }] + }; + this.switchLanguage = filename => { + const i18n = require('./data/node_requires/i18n.js'); + try { + window.languageJSON = i18n.loadLanguage(filename); + localStorage.appLanguage = filename; + window.signals.trigger('updateLocales'); + window.riot.update(); + } catch (e) { + alertify.alert('Could not open a language file: ' + e); + } + }; + var {switchLanguage} = this; + + fs.readdir('./data/i18n/') + .then(files => { + files.forEach(filename => { + if (path.extname(filename) !== '.json') { + return; + } + var file = filename.slice(0, -5); + if (file === 'Comments') { + return; + } + languageSubmenu.items.push({ + label: file, + icon: () => localStorage.appLanguage === file && 'check', + click: () => { + switchLanguage(file); + } + }); + }); + languageSubmenu.items.push({ + type: 'separator' + }); + languageSubmenu.items.push({ + label: window.languageJSON.common.translateToYourLanguage, + click: () => { + nw.Shell.openExternal('https://github.com/ct-js/ct-js/tree/develop/app/data/i18n'); + } + }); + }) + .catch(e => { + alertify.alert('Could not get i18n files: ' + e); }); \ No newline at end of file diff --git a/src/riotTags/method-selector.tag b/src/riotTags/method-selector.tag index 305d46e0e..d8d4cfc94 100644 --- a/src/riotTags/method-selector.tag +++ b/src/riotTags/method-selector.tag @@ -36,22 +36,22 @@ method-selector path = require('path'); const libsDir = './data/ct.libs'; - this.refreshModules = e => { + this.refreshModules = () => { this.inputProviders = []; const promises = []; for (const modName in global.currentProject.libs) { - promises.push( + const promise = fs.readJSON(path.join(libsDir, modName, 'module.json')) - .then(data => { - if (data.inputMethods) { + .then(catmod => { + if (catmod.inputMethods) { this.inputProviders.push({ - name: data.main.name, + name: catmod.main.name, code: modName, - methods: data.inputMethods + methods: catmod.inputMethods }); } - }) - ); + }); + promises.push(promise); } Promise.all(promises) .finally(() => { @@ -65,16 +65,16 @@ method-selector window.signals.off('modulesChanged', this.refreshModules); }); - this.selectMethod = code => e => { + this.selectMethod = code => () => { this.selectedMethod = code; }; - this.cancel = e => { + this.cancel = () => { this.searchString = ''; this.selectedMethod = ''; this.parent.addingMethod = false; this.parent.update(); }; - this.apply = e => { + this.apply = () => { this.opts.action.methods.push({ code: this.selectedMethod }); diff --git a/src/riotTags/modules-panel.tag b/src/riotTags/modules-panel.tag index 52b4a28a6..6d793dc61 100644 --- a/src/riotTags/modules-panel.tag +++ b/src/riotTags/modules-panel.tag @@ -83,7 +83,7 @@ modules-panel.panel.view use(xlink:href="data/icons.svg#alert-triangle") span {dependency} - #modinfohtml(if="{currentModuleHelp}" oncontextmenu="{onContextMenu}") + #modinfohtml(if="{currentModuleHelp}") raw(ref="raw" content="{currentModuleHelp}") h1(if="{currentModuleLicense}") {voc.license} pre(if="{currentModuleLicense}") @@ -133,7 +133,7 @@ modules-panel.panel.view div(class="desc" if="{field.help}") raw(ref="raw" content="{md.render(field.help)}") #modulehelp.tabbed.nbt(show="{tab === 'modulehelp'}" if="{currentModuleDocs}") - raw(ref="raw" content="{currentModuleDocs}" oncontextmenu="{onContextMenu}") + raw(ref="raw" content="{currentModuleDocs}") #modulelogs.tabbed.nbt(show="{tab === 'modulelogs'}" if="{currentModuleLogs}") h1 {voc.logs2} raw(ref="raw" content="{currentModuleLogs}") @@ -145,12 +145,14 @@ modules-panel.panel.view const md = require('markdown-it')({ html: false, linkify: true, - highlight: function (str, lang) { + highlight: function highlight(str, lang) { const hljs = require('highlight.js'); if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(lang, str).value; - } catch (__) {} + } catch (oO) { + void 0; + } } return ''; // use external default escaping } @@ -161,7 +163,6 @@ modules-panel.panel.view this.mixin(window.riotWired); this.namespace = 'modules'; this.mixin(window.riotVoc); - var exec = path.dirname(process.execPath).replace(/\\/g,'/'); this.currentModule = false; this.currentModuleHelp = ''; @@ -169,14 +170,14 @@ modules-panel.panel.view this.currentModuleLicense = ''; this.tab = 'moduleinfo'; - this.changeTab = tab => e => { + this.changeTab = tab => () => { this.tab = tab; }; this.allModules = []; this.on('update', () => { this.enabledModules = []; - for (let i in global.currentProject.libs) { + for (const i in global.currentProject.libs) { this.enabledModules.push(i); } }); @@ -190,20 +191,21 @@ modules-panel.panel.view const typedefPath = path.join(libsDir, moduleName, 'types.d.ts'); fs.pathExists(typedefPath) .then(exists => { + const ts = monaco.languages.typescript; if (!exists) { // generate dummy typedefs if none were provided by the module - const data = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${moduleName}: any; }`; + const catmodTypedefs = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${moduleName}: any; }`; glob.moduleTypings[moduleName] = [ - monaco.languages.typescript.javascriptDefaults.addExtraLib(data), - monaco.languages.typescript.typescriptDefaults.addExtraLib(data) + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) ]; return; } fs.readFile(typedefPath, 'utf8') - .then(data => { + .then(catmodTypedefs => { glob.moduleTypings[moduleName] = [ - monaco.languages.typescript.javascriptDefaults.addExtraLib(data), - monaco.languages.typescript.typescriptDefaults.addExtraLib(data) + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) ]; }); }); @@ -232,6 +234,23 @@ modules-panel.panel.view this.renderModule(this.allModules[0])(); this.update(); }); + + const addDefaults = moduleName => { + for (const field of this.currentModule.fields) { + if (!global.currentProject.libs[moduleName][field.key]) { + if (field.default) { + global.currentProject.libs[moduleName][field.key] = field.default; + } else if (field.type === 'number') { + global.currentProject.libs[moduleName][field.key] = 0; + } else if (field.type === 'checkbox') { + global.currentProject.libs[moduleName][field.key] = false; + } else { + global.currentProject.libs[moduleName][field.key] = ''; + } + } + } + }; + this.toggleModule = moduleName => e => { if (global.currentProject.libs[moduleName]) { delete global.currentProject.libs[moduleName]; @@ -240,30 +259,16 @@ modules-panel.panel.view global.currentProject.libs[moduleName] = {}; tryLoadTypedefs(moduleName); // 'Settings' page - if (this.currentModule.fields && global.currentProject.libs[name]) { - for (const field of this.currentModule.fields) { - if (!global.currentProject.libs[name][field.key]) { - if (field.default) { - global.currentProject.libs[name][field.key] = field.default; - } else { - if (field.type == 'number') { - global.currentProject.libs[name][field.key] = 0; - } else if (field.type == 'checkbox') { - global.currentProject.libs[name][field.key] = false; - } else { - global.currentProject.libs[name][field.key] = ''; - } - } - } - } + if (this.currentModule.fields) { + addDefaults(moduleName); } } this.renderModule(moduleName)(e); window.signals.trigger('modulesChanged'); glob.modified = true; }; - this.renderModule = name => e => { - fs.readJSON(path.join(libsDir, name, 'module.json'), (err, data) => { + this.renderModule = name => () => { + fs.readJSON(path.join(libsDir, name, 'module.json'), (err, catmod) => { if (err) { alertify.error(err); if (name in global.currentProject.libs) { @@ -273,7 +278,7 @@ modules-panel.panel.view this.update(); return; } - this.currentModule = data; + this.currentModule = catmod; this.currentModuleName = name; if (fs.pathExistsSync(path.join(libsDir, name, 'README.md'))) { @@ -310,21 +315,8 @@ modules-panel.panel.view this.tab = 'moduleinfo'; }; - this.onContextMenu = e => { - console.log(e); - }; - this.contextMenu = { - items: [{ - label: window.languageJSON.common.copy, - icon: 'copy', - click: () => { - - } - }] - }; - - this.importModules = async (e) => { - const files = e.target.files; + this.importModules = e => { + const {files} = e.target; if (files.length === 0) { return; } @@ -335,10 +327,11 @@ modules-panel.panel.view const entries = []; fs.createReadStream(value) .pipe(unzipper.Parse()) - .on('entry', async (entry) => { + .on('entry', async entry => { const fileName = entry.path.toLowerCase(); if (path.basename(fileName) === 'module.json') { - // okay, we consumes the entry by buffering the contents into memory. + // consume the entry by buffering the contents into memory. + // eslint-disable-next-line require-atomic-updates const content = entry.tmpContent = await entry.buffer(); const json = JSON.parse(content.toString()); moduleName = json.main.packageName || path.basename(value, '.zip'); @@ -351,11 +344,11 @@ modules-panel.panel.view }) .on('finish', async () => { if (moduleName !== null) { - // okay, let's create parent directory + // create a parent directory await fs.ensureDir(path.join(libsDir, moduleName)); - for (let entry of entries) { + for (const entry of entries) { const filePath = entry.path; - const indexOf = filePath.indexOf('/') + const indexOf = filePath.indexOf('/'); if (filePath === parentName) { continue; } @@ -368,21 +361,22 @@ modules-panel.panel.view } else { entry.path = `${moduleName}/${filePath}`; } - entry.path = path.join(libsDir, entry.path); + entry.path = path.join(libsDir, entry.path); // 'Directory' or 'File' if (entry.type === 'Directory') { + // eslint-disable-next-line no-await-in-loop await fs.ensureDir(entry.path); } else { const fileName = entry.path.toLowerCase(); - if (fileName.endsWith("module.json")) { + if (fileName.endsWith('module.json')) { const content = entry.tmpContent; + // eslint-disable-next-line no-await-in-loop await fs.writeFile(entry.path, content); } else { entry.pipe(fs.createWriteStream(entry.path)) .on('error', (e) => { alertify.error(e); - console.log(e); - return; + console.error(e); }); } } diff --git a/src/riotTags/new-project-onboarding.tag b/src/riotTags/new-project-onboarding.tag index d04cb1c7c..39f1ff378 100644 --- a/src/riotTags/new-project-onboarding.tag +++ b/src/riotTags/new-project-onboarding.tag @@ -32,18 +32,18 @@ new-project-onboarding this.namespace = 'onboarding'; this.mixin(window.riotVoc); - this.close = e => { + this.close = () => { delete sessionStorage.showOnboarding; this.parent.update(); }; - this.openDocs = link => e => { + this.openDocs = link => () => { window.signals.trigger('openDocs', { path: link || '/' }); this.close(); }; - this.toggleOnboarding = e => { - localStorage.showOnboarding = localStorage.showOnboarding !== 'off'? 'off' : 'on'; + this.toggleOnboarding = () => { + localStorage.showOnboarding = localStorage.showOnboarding !== 'off' ? 'off' : 'on'; }; \ No newline at end of file diff --git a/src/riotTags/notepad-panel.tag b/src/riotTags/notepad-panel.tag index 161b3c60a..f89837d19 100644 --- a/src/riotTags/notepad-panel.tag +++ b/src/riotTags/notepad-panel.tag @@ -1,126 +1,125 @@ -notepad-panel#notepad.panel.dockright(class="{opened: opened}") - ul.nav.tabs.nogrow.nb - li(onclick="{changeTab('notepadlocal')}" class="{active: tab === 'notepadlocal'}") - svg.feather - use(xlink:href="data/icons.svg#edit") - span {voc.local} - li(onclick="{changeTab('notepadglobal')}" class="{active: tab === 'notepadglobal'}") - svg.feather - use(xlink:href="data/icons.svg#clipboard") - span {voc.global} - li(onclick="{changeTab('helppages')}" class="{active: tab === 'helppages'}") - svg.feather - use(xlink:href="data/icons.svg#life-buoy") - span {voc.helppages} - div - div(show="{tab === 'notepadlocal'}") - .aCodeEditor(ref="notepadlocal") - div(show="{tab === 'notepadglobal'}") - .aCodeEditor(ref="notepadglobal") - div(show="{tab === 'helppages'}") - iframe(src="http://localhost:{server.address().port}/{getIfDarkTheme()? '?darkTheme=yep' : ''}" ref="helpIframe" nwdisable nwfaketop) - button.aHomeButton(title="{voc.backToHome}" onclick="{backToHome}") - svg.feather - use(xlink:href="data/icons.svg#home") - - button.vertical.dockleft(onclick="{notepadToggle}") - svg.feather - use(xlink:href="data/icons.svg#{opened? 'chevron-right' : 'chevron-left'}") - script. - const glob = require('./data/node_requires/glob'); - const hotkey = require('./data/node_requires/hotkeys')(document); - this.opened = false; - this.namespace = 'notepad'; - this.mixin(window.riotVoc); - this.notepadToggle = function() { - this.opened = !this.opened; - }; - - hotkey.on('F1', () => { - this.opened = true; - this.tab = 'helppages'; - this.update(); - }); - - this.tab = 'notepadlocal'; - this.changeTab = tab => e => { - this.tab = tab; - }; - this.on('update', () => { - setTimeout(() => { - if (this.tab && this.refs[this.tab] && this.refs[this.tab].codeEditor) { - this.refs[this.tab].codeEditor.layout(); - this.refs[this.tab].codeEditor.focus(); - } - }, 0); - }); - const updateEditorSize = () => { - if (this.tab && this.refs[this.tab]) { - this.refs[this.tab].codeEditor.layout(); - } - }; - window.addEventListener('resize', updateEditorSize); - this.on('unmount', () => { - window.removeEventListener('resize', updateEditorSize); - }); - - this.getIfDarkTheme = () => { - return localStorage.UItheme === 'Night' || localStorage.UItheme === 'Horizon'; - }; - - this.backToHome = e => { - this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}/`; - }; - - this.on('update', () => { - this.notepadlocal.setValue(global.currentProject.notes || ''); - }); - - this.on('mount', () => { - setTimeout(() => { - this.notepadlocal = window.setupCodeEditor(this.refs.notepadlocal, { - language: 'typescript' - }); - this.notepadglobal = window.setupCodeEditor(this.refs.notepadglobal, { - language: 'typescript' - }); - - this.notepadlocal.onDidChangeModelContent((e) => { - global.currentProject.notes = this.notepadlocal.getValue(); - glob.modified = true; - }); - this.notepadglobal.onDidChangeModelContent((e) => { - localStorage.notes = this.notepadglobal.getValue(); - }); - this.notepadglobal.setValue(localStorage.notes); - }, 0); - }); - this.on('unmount', () => { - // Manually destroy the editors to free up the memory - this.notepadlocal.dispose(); - this.notepadglobal.dispose(); - }); - - const nstatic = require('node-static'); - const fileServer = new nstatic.Server('data/docs/', { - cache: false, - serverInfo: 'ctjsgameeditor' - }); - - this.server = require('http').createServer(function (request, response) { - request.addListener('end', function () { - fileServer.serve(request, response); - }).resume(); - }); - this.server.listen(0); - - var openDocs = e => { - this.changeTab('helppages')(); - this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}${e.path || '/'}`; - this.opened = true; - this.update(); - }; - window.signals.on('openDocs', openDocs); - this.on('unmount', () => { - window.signals.off('openDocs', openDocs); +notepad-panel#notepad.panel.dockright(class="{opened: opened}") + ul.nav.tabs.nogrow.nb + li(onclick="{changeTab('notepadlocal')}" class="{active: tab === 'notepadlocal'}") + svg.feather + use(xlink:href="data/icons.svg#edit") + span {voc.local} + li(onclick="{changeTab('notepadglobal')}" class="{active: tab === 'notepadglobal'}") + svg.feather + use(xlink:href="data/icons.svg#clipboard") + span {voc.global} + li(onclick="{changeTab('helppages')}" class="{active: tab === 'helppages'}") + svg.feather + use(xlink:href="data/icons.svg#life-buoy") + span {voc.helppages} + div + div(show="{tab === 'notepadlocal'}") + .aCodeEditor(ref="notepadlocal") + div(show="{tab === 'notepadglobal'}") + .aCodeEditor(ref="notepadglobal") + div(show="{tab === 'helppages'}") + iframe(src="http://localhost:{server.address().port}/{getIfDarkTheme()? '?darkTheme=yep' : ''}" ref="helpIframe" nwdisable nwfaketop) + button.aHomeButton(title="{voc.backToHome}" onclick="{backToHome}") + svg.feather + use(xlink:href="data/icons.svg#home") + + button.vertical.dockleft(onclick="{notepadToggle}") + svg.feather + use(xlink:href="data/icons.svg#{opened? 'chevron-right' : 'chevron-left'}") + script. + const glob = require('./data/node_requires/glob'); + const hotkey = require('./data/node_requires/hotkeys')(document); + this.opened = false; + this.namespace = 'notepad'; + this.mixin(window.riotVoc); + this.notepadToggle = function notepadToggle() { + this.opened = !this.opened; + }; + + hotkey.on('F1', () => { + this.opened = true; + this.tab = 'helppages'; + this.update(); + }); + + this.tab = 'notepadlocal'; + this.changeTab = tab => () => { + this.tab = tab; + }; + this.on('update', () => { + setTimeout(() => { + if (this.tab && this.refs[this.tab] && this.refs[this.tab].codeEditor) { + this.refs[this.tab].codeEditor.layout(); + this.refs[this.tab].codeEditor.focus(); + } + }, 0); + }); + const updateEditorSize = () => { + if (this.tab && this.refs[this.tab]) { + this.refs[this.tab].codeEditor.layout(); + } + }; + window.addEventListener('resize', updateEditorSize); + this.on('unmount', () => { + window.removeEventListener('resize', updateEditorSize); + }); + + this.getIfDarkTheme = () => + localStorage.UItheme === 'Night' || localStorage.UItheme === 'Horizon'; + + this.backToHome = () => { + this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}/`; + }; + + this.on('update', () => { + this.notepadlocal.setValue(global.currentProject.notes || ''); + }); + + this.on('mount', () => { + setTimeout(() => { + this.notepadlocal = window.setupCodeEditor(this.refs.notepadlocal, { + language: 'typescript' + }); + this.notepadglobal = window.setupCodeEditor(this.refs.notepadglobal, { + language: 'typescript' + }); + + this.notepadlocal.onDidChangeModelContent(() => { + global.currentProject.notes = this.notepadlocal.getValue(); + glob.modified = true; + }); + this.notepadglobal.onDidChangeModelContent(() => { + localStorage.notes = this.notepadglobal.getValue(); + }); + this.notepadglobal.setValue(localStorage.notes); + }, 0); + }); + this.on('unmount', () => { + // Manually destroy the editors to free up the memory + this.notepadlocal.dispose(); + this.notepadglobal.dispose(); + }); + + const nstatic = require('node-static'); + const fileServer = new nstatic.Server('data/docs/', { + cache: false, + serverInfo: 'ctjsgameeditor' + }); + + this.server = require('http').createServer(function staticServerHandler(request, response) { + request.addListener('end', function serveFile() { + fileServer.serve(request, response); + }).resume(); + }); + this.server.listen(0); + + var openDocs = e => { + this.changeTab('helppages')(); + this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}${e.path || '/'}`; + this.opened = true; + this.update(); + }; + window.signals.on('openDocs', openDocs); + this.on('unmount', () => { + window.signals.off('openDocs', openDocs); }); \ No newline at end of file diff --git a/src/riotTags/particles/emitter-editor.tag b/src/riotTags/particles/emitter-editor.tag index 2d227cdcc..bc2b8e75b 100644 --- a/src/riotTags/particles/emitter-editor.tag +++ b/src/riotTags/particles/emitter-editor.tag @@ -426,9 +426,7 @@ emitter-editor.panel.pad this.mixin(window.riotWired); const {getTexturePreview, getTextureFromName, importImageToTexture} = require('./data/node_requires/resources/textures'); - this.getPreview = () => { - return getTexturePreview(this.opts.emitter.texture); - }; + this.getPreview = () => getTexturePreview(this.opts.emitter.texture); this.wireAndReset = path => e => { this.wire(path)(e); @@ -440,7 +438,8 @@ emitter-editor.panel.pad const emtInst = this.opts.emittermap[this.opts.emitter.uid]; if (useDirectValue) { if (field === '_frequency') { - emtInst[field] = Math.max(0.001, val); // otherwise it results into an infinite loop + crash + // otherwise it results into an infinite loop + crash + emtInst[field] = Math.max(0.001, val); } else { emtInst[field] = val; } @@ -474,7 +473,7 @@ emitter-editor.panel.pad window.signals.trigger('emitterResetRequest'); }; - this.updateScaleCurve = curve => { + this.updateScaleCurve = () => { if (this.opts.emittermap && (this.opts.emitter.uid in this.opts.emittermap)) { const emtInst = this.opts.emittermap[this.opts.emitter.uid]; const {PropertyNode} = PIXI.particles; @@ -483,7 +482,7 @@ emitter-editor.panel.pad window.signals.trigger('emitterResetRequest'); } }; - this.updateSpeedCurve = curve => { + this.updateSpeedCurve = () => { if (this.opts.emittermap && (this.opts.emitter.uid in this.opts.emittermap)) { const emtInst = this.opts.emittermap[this.opts.emitter.uid]; const {PropertyNode} = PIXI.particles; @@ -492,7 +491,7 @@ emitter-editor.panel.pad window.signals.trigger('emitterResetRequest'); } }; - this.updateColorCurve = (alphaCurve, colorCurve) => { + this.updateColorCurve = () => { if (this.opts.emittermap && (this.opts.emitter.uid in this.opts.emittermap)) { const emtInst = this.opts.emittermap[this.opts.emitter.uid]; const {PropertyNode} = PIXI.particles; @@ -505,12 +504,13 @@ emitter-editor.panel.pad }; /* Expects color and alpha to be the same length */ this.combineAlphaAndColors = () => { - const Color = net.brehaut.Color; + /* global net */ + const brehautColor = net.brehaut.Color; const combinedList = []; const emt = this.opts.emitter.settings; const l = Math.min(emt.color.list.length, emt.alpha.list.length); for (let i = 0; i < l; i++) { - const color = Color('#'+emt.color.list[i].value); + const color = brehautColor('#' + emt.color.list[i].value); color.alpha = emt.alpha.list[i].value; combinedList.push({ value: color.toString(), @@ -538,7 +538,7 @@ emitter-editor.panel.pad y: 0, r: 200, minR: 100 - } + }; } else if (type === 'burst') { emt.particlesPerWave = 5; emt.particleSpacing = 360 / 5; @@ -549,24 +549,24 @@ emitter-editor.panel.pad window.signals.trigger('emitterResetRequest'); }; - this.saveSectionState = (opened, tag) => { + this.saveSectionState = (opened, sectionTag) => { if (opened) { - if (!this.opts.emitter.openedTabs.includes(tag.opts.key)) { - this.opts.emitter.openedTabs.push(tag.opts.key); + if (!this.opts.emitter.openedTabs.includes(sectionTag.opts.key)) { + this.opts.emitter.openedTabs.push(sectionTag.opts.key); } } else { - const ind = this.opts.emitter.openedTabs.indexOf(tag.opts.key); + const ind = this.opts.emitter.openedTabs.indexOf(sectionTag.opts.key); if (ind !== -1) { this.opts.emitter.openedTabs.splice(ind, 1); } } }; - this.showTexturesSelector = e => { + this.showTexturesSelector = () => { this.pickingTexture = true; this.update(); }; - this.onTexturePicked = texture => e => { + this.onTexturePicked = texture => () => { const emt = this.opts.emitter; emt.texture = texture.uid; this.pickingTexture = false; @@ -578,15 +578,15 @@ emitter-editor.panel.pad this.update(); }; - this.showTextureImport = e => { + this.showTextureImport = () => { this.importingTexture = true; this.update(); }; - this.onTextureImport = texture => e => { + this.onTextureImport = texture => () => { try { getTextureFromName(texture.name); // a texture with the same name already exists; show an error - alertify.error(this.voc.alreadyHasAnImportingTexture.replace('$1', texture.name)); + window.alertify.error(this.voc.alreadyHasAnImportingTexture.replace('$1', texture.name)); return false; } catch (e) { // No such texture; add it. @@ -597,14 +597,15 @@ emitter-editor.panel.pad this.update(); window.signals.trigger('emitterResetRequest'); }); + return true; } }; - this.onTextureImportCancel = e => { + this.onTextureImportCancel = () => { this.importingTexture = false; this.update(); }; - this.deleteEmitter = e => { + this.deleteEmitter = () => { this.parent.deleteEmitter(this.opts.emitter); }; \ No newline at end of file diff --git a/src/riotTags/particles/emitter-tandem-editor.tag b/src/riotTags/particles/emitter-tandem-editor.tag index f8bf16263..ec0b21d85 100644 --- a/src/riotTags/particles/emitter-tandem-editor.tag +++ b/src/riotTags/particles/emitter-tandem-editor.tag @@ -61,7 +61,8 @@ emitter-tandem-editor.panel.view.flexrow ) texture-selector(if="{pickingPreviewTexture}" showempty="yes" onselected="{onPreviewTexturePicked}" oncancelled="{onPreviewTextureCancel}") script. - const Color = net.brehaut.Color; + /* global net */ + const brehautColor = net.brehaut.Color; this.tandem = this.opts.tandem; @@ -98,7 +99,9 @@ emitter-tandem-editor.panel.view.flexrow emitter.update(-emitterData.settings.delay); } else if (emitterData.settings.delay > 0) { // this needs to be delayed emitter.emit = false; - setTimeout(() => { // will capture the emitter in memory and create a temporary leak, but if delays are not longer than hours, we can ignore it + // will capture the emitter in memory and create a temporary leak, + // but if delays are not longer than hours, we can ignore it + setTimeout(() => { emitter.emit = true; }, emitterData.settings.delay * 1000); } @@ -132,19 +135,19 @@ emitter-tandem-editor.panel.view.flexrow } this.emitterInstances = []; this.uidToEmitterMap = {}; - await Promise.all( - this.tandem.emitters - .map(emitterData => this.spawnEmitter(emitterData, this.emitterContainer)) - ).then(emitters => - this.emitterInstances.push(...emitters) - ); + const promisesSpawnEmitters = this.tandem.emitters + .map(emitterData => this.spawnEmitter(emitterData, this.emitterContainer)); + await Promise.all(promisesSpawnEmitters) + .then(emitters => this.emitterInstances.push(...emitters)); if (this.refs.canvas) { const box = this.refs.canvas.getBoundingClientRect(); this.visualizersContainer.x = box.width / 2; this.visualizersContainer.y = box.height / 2; } this.generateShapeVisualizers(); - this.update(); // Need to update the riot tag so that editors get their link to emitter instances + // Need to update the riot tag + // so that editors get their link to emitter instances + this.update(); }; // Advances emitter simulation by a given amount of seconds this.updateEmitters = seconds => { @@ -157,12 +160,14 @@ emitter-tandem-editor.panel.view.flexrow this.updatePreviewLayout = () => { if (this.renderer && this.refs.canvas) { const box = this.refs.preview.getBoundingClientRect(); - const canvas = this.refs.canvas; + const {canvas} = this.refs; canvas.width = Math.round(box.width); canvas.height = Math.round(box.height); this.renderer.resize(canvas.width, canvas.height); - this.emitterContainer.x = this.visualizersContainer.x = this.previewTexture.x = canvas.width / 2; - this.emitterContainer.y = this.visualizersContainer.y = this.previewTexture.y = canvas.height / 2; + this.emitterContainer.x = canvas.width / 2; + this.emitterContainer.y = canvas.height / 2; + this.visualizersContainer.x = this.previewTexture.x = this.emitterContainer.x; + this.visualizersContainer.y = this.previewTexture.y = this.emitterContainer.y; this.emitterContainer.scale.x = this.emitterContainer.scale.y = this.visualizersContainer.scale.x = @@ -189,6 +194,8 @@ emitter-tandem-editor.panel.view.flexrow if (!emitter.showShapeVisualizer) { continue; } + const emitterX = emitter.settings.pos.x, + emitterY = emitter.settings.pos.y; if (emitter.settings.spawnType === 'point') { const crosshair = new PIXI.Graphics(); crosshair.lineStyle(2, 0x446adb, 1); @@ -196,17 +203,17 @@ emitter-tandem-editor.panel.view.flexrow crosshair.lineTo(0, 64); crosshair.moveTo(-64, 0); crosshair.lineTo(64, 0); - crosshair.x = emitter.settings.pos.x; - crosshair.y = emitter.settings.pos.y; + crosshair.x = emitterX; + crosshair.y = emitterY; this.visualizersContainer.addChild(crosshair); } else if (emitter.settings.spawnType === 'circle' || emitter.settings.spawnType === 'ring') { const circle = new PIXI.Graphics(); circle.lineStyle(2, 0x446adb, 1); circle.beginFill(0x446adb, 0.27); - circle.drawCircle(emitter.settings.pos.x, emitter.settings.pos.y, emitter.settings.spawnCircle.r); + circle.drawCircle(emitterX, emitterY, emitter.settings.spawnCircle.r); if (emitter.settings.spawnType === 'ring') { circle.beginHole(); - circle.drawCircle(emitter.settings.pos.x, emitter.settings.pos.y, emitter.settings.spawnCircle.minR); + circle.drawCircle(emitterX, emitterY, emitter.settings.spawnCircle.minR); circle.endHole(); } circle.endFill(); @@ -216,10 +223,10 @@ emitter-tandem-editor.panel.view.flexrow rect.lineStyle(2, 0x446adb, 1); rect.beginFill(0x446adb, 0.27); rect.drawRect( - emitter.settings.pos.x + emitter.settings.spawnRect.x, - emitter.settings.pos.y + emitter.settings.spawnRect.y, + emitterX + emitter.settings.spawnRect.x, + emitterY + emitter.settings.spawnRect.y, emitter.settings.spawnRect.w, - emitter.settings.spawnRect.h, + emitter.settings.spawnRect.h ); rect.endFill(); this.visualizersContainer.addChild(rect); @@ -227,8 +234,7 @@ emitter-tandem-editor.panel.view.flexrow const crosshair = new PIXI.Graphics(); crosshair.lineStyle(2, 0x446adb, 1); crosshair.drawStar( - emitter.settings.pos.x, - emitter.settings.pos.y, + emitterX, emitterY, emitter.settings.particlesPerWave, 64, 16, Math.PI * (0.5 + emitter.settings.angleStart / 180) @@ -238,13 +244,13 @@ emitter-tandem-editor.panel.view.flexrow } }; - this.updateGrid = size => { + this.updateGrid = () => { if (!this.grid || !this.emitterContainer) { return; } - const dark = Color(this.previewColor).getLuminance() > 0.5; - this.grid.blendMode = dark? PIXI.BLEND_MODES.MULTIPLY : PIXI.BLEND_MODES.ADD; + const dark = brehautColor(this.previewColor).getLuminance() > 0.5; + this.grid.blendMode = dark ? PIXI.BLEND_MODES.MULTIPLY : PIXI.BLEND_MODES.ADD; this.grid.width = this.refs.canvas.width; this.grid.height = this.refs.canvas.height; @@ -254,7 +260,7 @@ emitter-tandem-editor.panel.view.flexrow this.grid.texture = this.gridGen([ this.gridSize[0] * this.zoom, this.gridSize[1] * this.zoom - ], dark? '#ddd' : '#222'); + ], dark ? '#ddd' : '#222'); }; this.on('mount', () => { @@ -276,7 +282,7 @@ emitter-tandem-editor.panel.view.flexrow PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; } - this.renderer.renderer.backgroundColor = Number('0x'+this.previewColor.slice(1)); + this.renderer.renderer.backgroundColor = Number('0x' + this.previewColor.slice(1)); this.visualizersContainer = new PIXI.Container(); this.previewTexture = new PIXI.Sprite(PIXI.Texture.EMPTY); this.previewTexture.alpha = 0.5; @@ -293,7 +299,7 @@ emitter-tandem-editor.panel.view.flexrow this.updateEmitters(delta / (this.renderer.ticker.maxFPS || 60)); this.inspector.text = `FPS: ${Math.round(1 / this.renderer.ticker.deltaMS * 1000)} / ${this.renderer.ticker.maxFPS || 60}\n` + this.emitterInstances.map(e => `${e.particleCount} / ${e.maxParticles}`).join('\n') + - (this.complete? '\n'+this.voc.inspectorComplete : ''); + (this.complete ? '\n' + this.voc.inspectorComplete : ''); }); this.renderer.stage.addChild(this.previewTexture); this.renderer.stage.addChild(this.grid); @@ -324,12 +330,12 @@ emitter-tandem-editor.panel.view.flexrow /* UI events */ - this.addEmitter = e => { + this.addEmitter = () => { const defaultEmitter = require('./data/node_requires/resources/particles/defaultEmitter').get(); this.tandem.emitters.push(defaultEmitter); this.resetEmitters(); }; - this.changePreviewBg = e => { + this.changePreviewBg = () => { this.changingPreviewColor = !this.changingPreviewColor; if (this.changingPreviewColor) { this.oldPreviewColor = this.previewColor; @@ -337,7 +343,7 @@ emitter-tandem-editor.panel.view.flexrow }; this.updatePreviewColor = (color, evtype) => { this.previewColor = localStorage.tandemEditorPreviewBg = color; - this.renderer.renderer.backgroundColor = Number('0x'+color.slice(1)); + this.renderer.renderer.backgroundColor = Number('0x' + color.slice(1)); if (evtype === 'onapply') { this.changingPreviewColor = false; } @@ -347,11 +353,11 @@ emitter-tandem-editor.panel.view.flexrow this.cancelPreviewColor = () => { this.changingPreviewColor = false; this.previewColor = localStorage.tandemEditorPreviewBg = this.oldPreviewColor; - this.renderer.renderer.backgroundColor = Number('0x'+this.previewColor.slice(1)); + this.renderer.renderer.backgroundColor = Number('0x' + this.previewColor.slice(1)); this.update(); }; - this.changeGrid = e => { - alertify + this.changeGrid = () => { + window.alertify .confirm(`${this.voc.newGridSize}
x `) .then(e => { if (e.buttonClicked === 'ok') { @@ -363,10 +369,10 @@ emitter-tandem-editor.panel.view.flexrow }); }; - this.openPreviewTexturePicker = e => { + this.openPreviewTexturePicker = () => { this.pickingPreviewTexture = true; }; - this.onPreviewTexturePicked = texture => e => { + this.onPreviewTexturePicked = texture => () => { this.tandem.previewTexture = texture.uid; this.pickingPreviewTexture = false; this.updatePreviewLayout(); @@ -380,7 +386,7 @@ emitter-tandem-editor.panel.view.flexrow /* Zoom in/out by clicking buttons and scrolling mouse wheel */ this.zoom = 1; - this.setZoom = zoom => e => { + this.setZoom = zoom => () => { this.zoom = zoom; if (this.emitterContainer) { this.emitterContainer.scale.x = this.emitterContainer.scale.y = this.zoom; @@ -401,19 +407,16 @@ emitter-tandem-editor.panel.view.flexrow } else if (this.zoom === 0.125) { this.zoom = 0.25; } - } else { - // out - if (this.zoom === 4) { - this.zoom = 2; - } else if (this.zoom === 2) { - this.zoom = 1; - } else if (this.zoom === 1) { - this.zoom = 0.5; - } else if (this.zoom === 0.5) { - this.zoom = 0.25; - } else if (this.zoom === 0.25) { - this.zoom = 0.125; - } + } else if (this.zoom === 4) { // out + this.zoom = 2; + } else if (this.zoom === 2) { + this.zoom = 1; + } else if (this.zoom === 1) { + this.zoom = 0.5; + } else if (this.zoom === 0.5) { + this.zoom = 0.25; + } else if (this.zoom === 0.25) { + this.zoom = 0.125; } this.emitterContainer.scale.x = this.emitterContainer.scale.y = @@ -451,8 +454,9 @@ emitter-tandem-editor.panel.view.flexrow */ const minSizeW = 20 * 16; const getMaxSizeW = () => window.innerWidth - 128; - this.panelWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), localStorage.particlesPanelWidth || 20 * 32)); - this.gutterMouseDown = e => { + const savedPanelWidth = localStorage.particlesPanelWidth || (20 * 32); + this.panelWidth = Math.max(minSizeW, Math.min(savedPanelWidth, getMaxSizeW())); + this.gutterMouseDown = () => { this.draggingGutter = true; }; const gutterMove = e => { @@ -476,7 +480,7 @@ emitter-tandem-editor.panel.view.flexrow document.removeEventListener('mouseup', gutterUp); }); - this.apply = e => { + this.apply = () => { this.parent.editingTandem = false; this.parent.update(); window.signals.trigger('tandemUpdated', this.tandem); diff --git a/src/riotTags/particles/fx-panel.tag b/src/riotTags/particles/fx-panel.tag index 7dfa5e98d..10040cfd3 100644 --- a/src/riotTags/particles/fx-panel.tag +++ b/src/riotTags/particles/fx-panel.tag @@ -19,11 +19,11 @@ fx-panel.panel.view this.namespace = 'particleEmitters'; this.mixin(window.riotVoc); - this.thumbnails = tandem => `data/img/particles.png`; + this.thumbnails = () => 'data/img/particles.png'; // Technically we edit a number of emitters at once — a "tandem", // but to not overcomplicate it all, let's call them "emitters" in UI anyways. - this.openTandem = tandem => e => { + this.openTandem = tandem => () => { this.editingTandem = true; this.editedTandem = tandem; this.update(); @@ -35,16 +35,15 @@ fx-panel.panel.view e.preventDefault(); }; - this.emitterTandemCreate = e => { + this.emitterTandemCreate = () => { const defaultEmitter = require('./data/node_requires/resources/particles/defaultEmitter').get(); const generateGUID = require('./data/node_requires/generateGUID'); - let id = generateGUID(), - slice = id.split('-').pop(); + const id = generateGUID(), + slice = id.split('-').pop(); const tandem = { name: 'Tandem_' + slice, origname: 'pt' + slice, - emitters: [defaultEmitter] }; @@ -53,13 +52,13 @@ fx-panel.panel.view this.editedTandem = tandem; }; - this.setUpPanel = e => { + this.setUpPanel = () => { this.refs.emitterTandems.updateList(); this.editingTandem = false; this.editedTandem = null; this.update(); }; - this.updatePanel = e => { + this.updatePanel = () => { if (this.refs.emitterTandems) { this.refs.emitterTandems.updateList(); this.update(); @@ -77,19 +76,19 @@ fx-panel.panel.view this.tandemMenu = { items: [{ label: window.languageJSON.common.open, - click: e => { + click: () => { this.editingTandem = true; this.update(); } }, { - label: languageJSON.common.copyName, - click: e => { + label: window.languageJSON.common.copyName, + click: () => { nw.Clipboard.get().set(this.editedTandem.name, 'text'); } }, { label: window.languageJSON.common.duplicate, click: () => { - alertify + window.alertify .defaultValue(this.editedTandem.name + '_dup') .prompt(window.languageJSON.common.newname) .then(e => { @@ -113,7 +112,7 @@ fx-panel.panel.view }, { label: window.languageJSON.common.rename, click: () => { - alertify + window.alertify .defaultValue(this.editedTandem.name) .prompt(window.languageJSON.common.newname) .then(e => { @@ -128,7 +127,7 @@ fx-panel.panel.view }, { label: window.languageJSON.common.delete, click: () => { - alertify + window.alertify .okBtn(window.languageJSON.common.delete) .cancelBtn(window.languageJSON.common.cancel) .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editedTandem.name)) @@ -138,7 +137,7 @@ fx-panel.panel.view global.currentProject.emitterTandems.splice(ind, 1); this.refs.emitterTandems.updateList(); this.update(); - alertify + window.alertify .okBtn(window.languageJSON.common.ok) .cancelBtn(window.languageJSON.common.cancel); } diff --git a/src/riotTags/patreon-screen.tag b/src/riotTags/patreon-screen.tag index 0940a944c..7e153703b 100644 --- a/src/riotTags/patreon-screen.tag +++ b/src/riotTags/patreon-screen.tag @@ -69,9 +69,25 @@ patreon-screen.view(style="z-index: 100;") this.mixin(window.riotVoc); this.loading = true; this.emojis = [ - '😊', '😋', '😍', '😘', '🥰', '😗', '😙', '😚', - '🥳', '🤪', '🐱', '😻', ' 😽', '😸', '🎂', '🥂', - '🌞', '🎊', '🎉' + '😊', + '😋', + '😍', + '😘', + '🥰', + '😗', + '😙', + '😚', + '🥳', + '🤪', + '🐱', + '😻', + '😽', + '😸', + '🎂', + '🥂', + '🌞', + '🎊', + '🎉' ]; this.confettiColors = [ '#ffd300', @@ -84,23 +100,23 @@ patreon-screen.view(style="z-index: 100;") astronauts: [], programmers: [] }; - const getMagicNumber = str => { - return str.split('').map(char => char.codePointAt(0)).reduce((sum, x) => sum + x); - }; - this.getEmoji = str => { - return this.emojis[getMagicNumber(str) % this.emojis.length]; - }; - this.getFiller = str => { - return this.voc.aboutFillers[getMagicNumber(str) % this.voc.aboutFillers.length]; - }; - this.getConfettiColor = () => this.confettiColors[Math.floor(Math.random() * this.confettiColors.length)]; + const getMagicNumber = str => + str.split('') + .map(char => char.codePointAt(0)) + .reduce((sum, x) => sum + x); + this.getEmoji = str => + this.emojis[getMagicNumber(str) % this.emojis.length]; + this.getFiller = str => + this.voc.aboutFillers[getMagicNumber(str) % this.voc.aboutFillers.length]; + this.getConfettiColor = () => + this.confettiColors[Math.floor(Math.random() * this.confettiColors.length)]; this.importPatronData = text => { const patrons = []; var table = text.split('\r\n').map(row => row.split(',')); for (let i = 1, l = table.length; i < l; i++) { const obj = {}, - row = table[i]; + row = table[i]; for (let j = 0; j < row.length; j++) { obj[table[0][j].trim()] = row[j]; } @@ -129,7 +145,7 @@ patreon-screen.view(style="z-index: 100;") this.loadPatrons = () => { this.loading = true; window.fetch('https://docs.google.com/spreadsheets/d/e/2PACX-1vTUMd6nvY0if8MuVDm5-zMfAxWCSWpUzOc81SehmBVZ6mytFkoB3y9i9WlUufhIMteMDc00O9EqifI3/pub?output=csv') - .then(data => data.text()) + .then(response => response.text()) .then(this.importPatronData) .catch(e => { console.error(e); @@ -145,6 +161,6 @@ patreon-screen.view(style="z-index: 100;") }; this.loadPatrons(); - this.openPatreon = e => { + this.openPatreon = () => { nw.Shell.openExternal('https://www.patreon.com/comigo'); - } \ No newline at end of file + }; \ No newline at end of file diff --git a/src/riotTags/project-selector.tag b/src/riotTags/project-selector.tag index c72d408e2..b5bc1eda2 100644 --- a/src/riotTags/project-selector.tag +++ b/src/riotTags/project-selector.tag @@ -81,7 +81,7 @@ project-selector /** * Update a splash image of a selected project */ - this.updatePreview = projectPath => e => { + this.updatePreview = projectPath => () => { this.projectSplash = 'file://' + path.dirname(projectPath) + '/' + path.basename(projectPath, '.ict') + '/img/splash.png'; }; /** @@ -91,10 +91,10 @@ project-selector */ this.newProject = async (way, codename) => { sessionStorage.showOnboarding = true; - const projectData = require('./data/node_requires/resources/projects/defaultProject').get(); + const defaultProject = require('./data/node_requires/resources/projects/defaultProject').get(); const YAML = require('js-yaml'); - const data = YAML.safeDump(projectData); - fs.outputFile(path.join(way, codename + '.ict'), data) + const projectYAML = YAML.safeDump(defaultProject); + fs.outputFile(path.join(way, codename + '.ict'), projectYAML) .catch(e => { alertify.error(this.voc.unableToWriteToFolders + '\n' + e); throw e; @@ -119,23 +119,23 @@ project-selector * Opens a recent project when an item in the Recent Project list is double-clicked */ this.loadRecentProject = e => { - var projectPath = e.item.project; + const projectPath = e.item.project; window.loadProject(projectPath); }; /** * Removes a project from the recents list */ this.forgetProject = e => { - var project = e.item.project; + const {project} = e.item; this.lastProjects.splice(this.lastProjects.indexOf(project), 1); localStorage.lastProjects = this.lastProjects.join(';'); e.stopPropagation(); - } + }; /** * Handler for a manual search for a project folder, triggered by an input[type="file"] */ - this.chooseProjectFolder = async e => { + this.chooseProjectFolder = async () => { const defaultProjectDir = require('./data/node_requires/resources/projects').getDefaultProjectDir(); const projPath = await window.showOpenDialog({ title: this.voc.newProject.selectProjectFolder, @@ -148,7 +148,7 @@ project-selector } }; - this.openProjectFolder = e => { + this.openProjectFolder = () => { const codename = this.refs.projectname.value; if (codename.length === 0) { alertify.error(this.voc.newProject.nameerr); @@ -160,7 +160,7 @@ project-selector /** * Handler for a manual search for a project, triggered by an input[type="file"] */ - this.openProjectFind = async e => { + this.openProjectFind = async () => { const defaultProjectDir = require('./data/node_requires/resources/projects').getDefaultProjectDir(); const proj = await window.showOpenDialog({ filter: '.ict', @@ -174,25 +174,30 @@ project-selector sessionStorage.projname = path.basename(proj); global.projdir = path.dirname(proj) + path.sep + path.basename(proj, '.ict'); } else { - alertify.error(languageJSON.common.wrongFormat); + alertify.error(window.languageJSON.common.wrongFormat); } }; // Checking for updates setTimeout(() => { const {isWin, isLinux} = require('./data/node_requires/platformUtils.js'); - const channel = isWin? 'win64' : (isLinux? 'linux64': 'osx64'); + let channel = 'osx64'; + if (isWin) { + channel = 'win64'; + } else if (isLinux) { + channel = 'linux64'; + } fetch(`https://itch.io/api/1/x/wharf/latest?target=comigo/ct&channel_name=${channel}`) - .then(data => data.json()) + .then(response => response.json()) .then(json => { if (!json.errors) { - if (this.ctjsVersion != json.latest) { + if (this.ctjsVersion !== json.latest) { this.newVersion = this.voc.latestVersion.replace('$1', json.latest); this.update(); } } else { console.error('Update check failed:'); - console.log(json.errors); + console.error(json.errors); } }); }, 0); diff --git a/src/riotTags/rooms/room-backgrounds-editor.tag b/src/riotTags/rooms/room-backgrounds-editor.tag index 12735dbd8..4d78d9bd6 100644 --- a/src/riotTags/rooms/room-backgrounds-editor.tag +++ b/src/riotTags/rooms/room-backgrounds-editor.tag @@ -68,23 +68,23 @@ room-backgrounds-editor.room-editor-Backgrounds.tabbed.tall this.parent.refreshRoomCanvas(); } }); - this.onTextureSelected = texture => e => { + this.onTextureSelected = texture => () => { this.editingBackground.texture = texture.uid; this.pickingBackground = false; this.creatingBackground = false; this.update(); }; - this.onTextureCancel = e => { + this.onTextureCancel = () => { this.pickingBackground = false; if (this.creatingBackground) { - let bgs = this.opts.room.backgrounds; + const bgs = this.opts.room.backgrounds; bgs.splice(bgs.indexOf(this.editingBackground), 1); this.parent.resortRoom(); this.creatingBackground = false; } this.update(); }; - this.addBg = function () { + this.addBg = () => { var newBg = { depth: 0, texture: -1, @@ -94,9 +94,7 @@ room-backgrounds-editor.room-editor-Backgrounds.tabbed.tall this.editingBackground = newBg; this.pickingBackground = true; this.creatingBackground = true; - this.opts.room.backgrounds.sort(function (a, b) { - return a.depth - b.depth; - }); + this.opts.room.backgrounds.sort((a, b) => a.depth - b.depth); this.parent.resortRoom(); this.update(); }; @@ -123,9 +121,7 @@ room-backgrounds-editor.room-editor-Backgrounds.tabbed.tall }; this.onChangeBgDepth = e => { e.item.background.depth = Number(e.target.value); - this.opts.room.backgrounds.sort(function (a, b) { - return a.depth - b.depth; - }); + this.opts.room.backgrounds.sort((a, b) => a.depth - b.depth); this.parent.resortRoom(); }; diff --git a/src/riotTags/rooms/room-editor.tag b/src/riotTags/rooms/room-editor.tag index a752a9ce2..bd9a9d5c5 100644 --- a/src/riotTags/rooms/room-editor.tag +++ b/src/riotTags/rooms/room-editor.tag @@ -1,680 +1,692 @@ -room-editor.panel.view - .toolbar.tall(style="width: {sidebarWidth}px") - .settings.nogrow.noshrink - b {voc.name} - br - input.wide(type="text" value="{room.name}" onchange="{wire('this.room.name')}") - .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} - .fifty.npt.npb.npl - b {voc.width} - br - input.wide(type="number" value="{room.width}" onchange="{wire('this.room.width')}") - .fifty.npt.npb.npr - b {voc.height} - br - input.wide(type="number" value="{room.height}" onchange="{wire('this.room.height')}") - br - button.wide(onclick="{openRoomEvents}") - svg.feather(if="{room.oncreate || room.onstep || room.ondestroy || room.ondraw}") - use(xlink:href="data/icons.svg#check") - span {voc.events} - .palette - .tabwrap - ul.tabs.nav.noshrink.nogrow - li(onclick="{changeTab('roomcopies')}" class="{active: tab === 'roomcopies'}") {voc.copies} - li(onclick="{changeTab('roombackgrounds')}" class="{active: tab === 'roombackgrounds'}") {voc.backgrounds} - li(onclick="{changeTab('roomtiles')}" class="{active: tab === 'roomtiles'}") {voc.tiles} - .relative - room-type-picker(show="{tab === 'roomcopies'}" current="{currentType}") - room-backgrounds-editor(show="{tab === 'roombackgrounds'}" room="{room}") - room-tile-editor(show="{tab === 'roomtiles'}" room="{room}") - .done.nogrow - button.wide#roomviewdone(onclick="{roomSave}") - svg.feather - use(xlink:href="data/icons.svg#check") - span {voc.done} - .aResizer.vertical(ref="gutter" onmousedown="{gutterMouseDown}") - .editor(ref="canvaswrap") - canvas( - ref="canvas" - onclick="{onCanvasClick}" - onmousedown="{onCanvasPress}" - onmousemove="{onCanvasMove}" - onmouseup="{onCanvasMouseUp}" - onmouseout="{refreshRoomCanvas}" - onmousewheel="{onCanvasWheel}" - oncontextmenu="{onCanvasContextMenu}" - ) - .shift - button.inline.square(title="{voc.shift}" onclick="{roomShift}") - svg.feather - use(xlink:href="data/icons.svg#move") - span(if="{window.innerWidth - sidebarWidth > 840}") {voc.hotkeysNotice} - .zoom - b(if="{window.innerWidth - sidebarWidth > 840}") {vocGlob.zoom} - div.button-stack - button#roomzoom12.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(0.125)}" class="{active: zoomFactor === 0.125}") 12% - button#roomzoom25.inline(onclick="{roomToggleZoom(0.25)}" class="{active: zoomFactor === 0.25}") 25% - button#roomzoom50.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(0.5)}" class="{active: zoomFactor === 0.5}") 50% - button#roomzoom100.inline(onclick="{roomToggleZoom(1)}" class="{active: zoomFactor === 1}") 100% - button#roomzoom200.inline(onclick="{roomToggleZoom(2)}" class="{active: zoomFactor === 2}") 200% - button#roomzoom400.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(4)}" class="{active: zoomFactor === 4}") 400% - .grid - button#roomgrid(onclick="{roomToggleGrid}" class="{active: room.gridX > 0}") - span {voc[room.gridX > 0? 'gridoff' : 'grid']} - .center - button#roomcenter(onclick="{roomToCenter}") {voc.tocenter} - span.aMouseCoord(if="{window.innerWidth - sidebarWidth > 470}" ref="mousecoords") ({mouseX}:{mouseY}) - room-events-editor(if="{editingCode}" room="{room}") - context-menu(menu="{roomCanvasCopiesMenu}" ref="roomCanvasCopiesMenu") - context-menu(menu="{roomCanvasMenu}" ref="roomCanvasMenu") - context-menu(menu="{roomCanvasTileMenu}" ref="roomCanvasTileMenu") - context-menu(menu="{roomCanvasTilesMenu}" ref="roomCanvasTilesMenu") - script. - const minSizeW = 250; - const getMaxSizeW = () => window.innerWidth - 300; - this.sidebarWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), localStorage.roomSidebarWidth || 300)); - - this.gutterMouseDown = e => { - this.draggingGutter = true; - }; - const gutterMove = e => { - if (!this.draggingGutter) { - return; - } - this.sidebarWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), e.clientX)); - localStorage.roomSidebarWidth = this.sidebarWidth; - this.update(); - var canvas = this.refs.canvas, - sizes = this.refs.canvaswrap.getBoundingClientRect(); - if (canvas.width != sizes.width || canvas.height != sizes.height) { - canvas.width = sizes.width; - canvas.height = sizes.height; - } - this.refreshRoomCanvas(); - }; - const gutterUp = () => { - if (this.draggingGutter) { - this.draggingGutter = false; - //updateCanvasSize(); - //document.body.removeChild(catcher); - } - }; - document.addEventListener('mousemove', gutterMove); - document.addEventListener('mouseup', gutterUp); - this.on('unmount', () => { - document.removeEventListener('mousemove', gutterMove); - document.removeEventListener('mouseup', gutterUp); - }); - - this.editingCode = false; - this.forbidDrawing = false; - const fs = require('fs-extra'); - const glob = require('./data/node_requires/glob'); - this.namespace = 'roomview'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.mixin(window.roomCopyTools); - this.mixin(window.roomTileTools); - - this.room = this.opts.room; - - this.mouseX = this.mouseY = 0; - this.roomx = this.room.width / 2; - this.roomy = this.room.height / 2; - this.zoomFactor = 1; - this.room.gridX = this.room.gridX || this.room.grid || 64; - this.room.gridY = this.room.gridY || this.room.grid || 64; - this.dragging = false; - this.tab = 'roomcopies'; - - var updateCanvasSize = e => { - // Firstly, check that we don't need to reflow the layout due to window shrinking - const oldSidebarWidth = this.sidebarWidth; - this.sidebarWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), this.sidebarWidth)); - if (oldSidebarWidth !== this.sidebarWidth) { - this.update(); - } - var canvas = this.refs.canvas, - sizes = this.refs.canvaswrap.getBoundingClientRect(); - if (canvas.width != sizes.width || canvas.height != sizes.height) { - canvas.width = sizes.width; - canvas.height = sizes.height; - } - setTimeout(this.refreshRoomCanvas, 10); - }; - this.on('update', () => { - if (global.currentProject.rooms.find(room => - this.room.name === room.name && this.room !== room - )) { - this.nameTaken = true; - } else { - this.nameTaken = false; - } - }); - this.on('mount', () => { - this.room = this.opts.room; - this.refs.canvas.x = this.refs.canvas.getContext('2d'); - this.gridCanvas = document.createElement('canvas'); - this.gridCanvas.x = this.gridCanvas.getContext('2d'); - this.redrawGrid(); - window.addEventListener('resize', updateCanvasSize); - updateCanvasSize(); - }); - this.on('unmount', () => { - window.removeEventListener('resize', updateCanvasSize); - }); - - this.openRoomEvents = e => { - this.editingCode = true; - }; - - // Навигация по комнате, настройки вида - this.roomToggleZoom = zoomFactor => e => { - this.zoomFactor = zoomFactor; - this.redrawGrid(); - this.refreshRoomCanvas(); - }; - this.roomToCenter = e => { - this.roomx = this.room.width / 2; - this.roomy = this.room.height / 2; - this.refreshRoomCanvas(); - }; - this.redrawGrid = () => { - this.gridCanvas.width = this.room.gridX; - this.gridCanvas.height = this.room.gridY; - this.gridCanvas.x.clearRect(0, 0, this.room.gridX, this.room.gridY); - this.gridCanvas.x.globalAlpha = 0.3; - this.gridCanvas.x.strokeStyle = localStorage.UItheme === 'Night'? '#44dbb5' : '#446adb'; - this.gridCanvas.x.lineWidth = 1 / this.zoomFactor; - this.gridCanvas.x.strokeRect(0.5 / this.zoomFactor, 0.5 / this.zoomFactor, this.room.gridX, this.room.gridY); - }; - this.roomToggleGrid = () => { - if (this.room.gridX === 0) { - alertify - .confirm(this.voc.gridsize + `
x `) - .then(e => { - if (e.buttonClicked === 'ok') { - this.room.gridX = Number(document.getElementById('theGridSizeX').value); - this.room.gridY = Number(document.getElementById('theGridSizeY').value); - } - this.redrawGrid(); - this.refreshRoomCanvas(); - this.update(); - }); - } else { - this.refreshRoomCanvas(); - this.room.gridX = 0; - this.room.gridY = 0; - } - }; - - // Работа с копиями - this.tab = 'roomcopies'; - this.changeTab = tab => e => { - this.tab = tab; - if (tab === 'roombackgrounds') { - this.roomUnpickType(); - } - }; - this.roomUnpickType = e => { - this.currentType = -1; - }; - - /** Преобразовать x на канвасе в x на комнате */ - this.xToRoom = x => (x - ~~(this.refs.canvas.width / 2)) / this.zoomFactor + this.roomx; - /** Преобразовать y на канвасе в y на комнате */ - this.yToRoom = y => (y - ~~(this.refs.canvas.height / 2)) / this.zoomFactor + this.roomy; - /** Преобразовать x в комнате в x на канвасе */ - this.xToCanvas = x => (x - this.roomx) * this.zoomFactor + ~~(this.refs.canvas.width / 2); - /** Преобразовать y в комнате в y на канвасе */ - this.yToCanvas = y => (y - this.roomy) * this.zoomFactor + ~~(this.refs.canvas.height / 2); - - this.onCanvasClick = e => { - if (this.tab === 'roomcopies') { - this.onCanvasClickCopies(e); - } else if (this.tab === 'roomtiles') { - this.onCanvasClickTiles(e); - } - }; - /** При нажатии на канвас, если не выбрана копия, то начинаем перемещение */ - this.onCanvasPress = e => { - this.mouseDown = true; - this.startx = e.offsetX; - this.starty = e.offsetY; - - if (this.tab === 'roomcopies' && this.onCanvasPressCopies(e)) { - return; - } - if ((this.currentType === -1 && !e.shiftKey && this.tab !== 'roomtiles' && e.button === 0 && !e.ctrlKey) - || e.button === 1) { - this.dragging = true; - } - }; - /** и безусловно прекращаем перемещение при отпускании мыши */ - this.onCanvasMouseUp = e => { - this.mouseDown = false; - this.lastCopyX = null; - this.lastCopyY = null; - this.lastTileX = null; - this.lastTileY = null; - if (this.dragging) { - this.dragging = false; - } else { - if (this.tab === 'roomtiles') { - this.onCanvasMouseUpTiles(e); - } else if (this.tab === 'roomcopies') { - this.onCanvasMouseUpCopies(e); - } - } - setTimeout(() => { - this.movingStuff = false; - }, 0); - }; - this.drawDeleteCircle = e => { - // Рисовка кружка для удаления копий - var maxdist = Math.max(this.room.gridX, this.room.gridY); - this.refreshRoomCanvas(e); - var x = this.refs.canvas.x; - x.fillStyle = '#F00'; - x.strokeStyle = '#000'; - x.globalAlpha = 0.5; - x.beginPath(); - x.arc(this.xToRoom(e.offsetX), this.yToRoom(e.offsetY), maxdist, 0, 2 * Math.PI); - x.fill(); - x.stroke(); - }; - - /** - * Updating mouse coordinates display at the bottom-left corner - */ - this.updateMouseCoords = function (e) { - var dx = Math.floor(this.xToRoom(e.offsetX)), - dy = Math.floor(this.yToRoom(e.offsetY)); - if (this.room.gridX === 0 || e.altKey) { - this.mouseX = dx; - this.mouseY = dy; - } else { - this.mouseX = Math.round(dx / this.room.gridX) * this.room.gridX; - this.mouseY = Math.round(dy / this.room.gridY) * this.room.gridY; - } - this.refs.mousecoords.innerHTML = `(${this.mouseX}:${this.mouseY})`; - }; - - /** Начинаем перемещение, или же показываем предварительное расположение новой копии */ - this.onCanvasMove = e => { - e.preventUpdate = true; - if (this.dragging && !this.movingStuff) { - // перетаскивание - this.roomx -= ~~(e.movementX / this.zoomFactor); - this.roomy -= ~~(e.movementY / this.zoomFactor); - this.refreshRoomCanvas(e); - } else if ( // если зажата мышь и клавиша Shift, то создавать больше копий/тайлов - e.shiftKey && this.mouseDown && - ( - (this.tab === 'roomcopies' && this.currentType !== -1) || - this.tab === 'roomtiles' - ) - ) { - this.onCanvasClick(e); - } else if (this.tab === 'roomcopies') { - this.onCanvasMoveCopies(e); - } else if (this.tab === 'roomtiles') { - this.onCanvasMoveTiles(e); - } - this.updateMouseCoords(e); - }; - - /** При прокрутке колёсиком меняем фактор зума */ - this.onCanvasWheel = e => { - if (e.wheelDelta > 0) { - // in - if (this.zoomFactor === 2) { - this.zoomFactor = 4; - } else if (this.zoomFactor === 1) { - this.zoomFactor = 2; - } else if (this.zoomFactor === 0.5) { - this.zoomFactor = 1; - } else if (this.zoomFactor === 0.25) { - this.zoomFactor = 0.5; - } else if (this.zoomFactor === 0.125) { - this.zoomFactor = 0.25; - } - } else { - // out - if (this.zoomFactor === 4) { - this.zoomFactor = 2; - } else if (this.zoomFactor === 2) { - this.zoomFactor = 1; - } else if (this.zoomFactor === 1) { - this.zoomFactor = 0.5; - } else if (this.zoomFactor === 0.5) { - this.zoomFactor = 0.25; - } else if (this.zoomFactor === 0.25) { - this.zoomFactor = 0.125; - } - } - this.redrawGrid(); - this.refreshRoomCanvas(e); - this.updateMouseCoords(e); - // this.update(); - }; - this.onCanvasContextMenu = e => { - this.dragging = false; - this.mouseDown = false; - if (this.tab === 'roomcopies') { - if (this.selectedCopies && this.selectedCopies.length) { - this.onCanvasContextMenuMultipleCopies(e); - } else { - this.onCanvasContextMenuCopies(e); - } - } else if (this.tab === 'roomtiles') { - if (this.selectedTiles && this.selectedTiles.length) { - this.onCanvasContextMenuMultipleTiles(e); - } else { - this.onCanvasContextMenuTiles(e); - } - } - e.preventDefault(); - return true; - }; - - // Shifts all the copies in a room at once. - this.roomShift = e => { - window.alertify.confirm(` - ${window.languageJSON.roomview.shifttext} - - - `) - .then((e, a) => { - if (e.buttonClicked === 'ok') { - var dx = Number(document.getElementById('roomshiftx').value) || 0, - dy = Number(document.getElementById('roomshifty').value) || 0; - for (const copy of this.room.copies) { - copy.x += dx; - copy.y += dy; - } - for (const tileLayer of this.room.tiles) { - for (const tile of tileLayer.tiles) { - tile.x += dx; - tile.y += dy; - } - } - this.refreshRoomCanvas(); - } - }); - }; - - /** Saves a room (in fact, just marks a project as an unsaved, and closes the room editor) */ - this.roomSave = e => { - if (this.nameTaken) { - // animate the error notice - require('./data/node_requires/jellify')(this.refs.errorNotice); - soundbox.play('Failure'); - return false; - } - this.room.lastmod = +(new Date()); - this.roomGenSplash() - .then(() => { - glob.modified = true; - this.parent.editing = false; - this.parent.update(); - }) - .catch(err => { - console.error(err); - glob.modified = true; - this.parent.editing = false; - this.parent.update(); - }); - }; - - this.resortRoom = () => { - // Make an array of all the backgrounds, tile layers and copies, and then sort it. - this.stack = this.room.copies.concat(this.room.backgrounds).concat(this.room.tiles); - this.stack.sort((a, b) => { - let depthA = a.depth !== void 0? a.depth : global.currentProject.types[glob.typemap[a.uid]].depth, - depthB = b.depth !== void 0? b.depth : global.currentProject.types[glob.typemap[b.uid]].depth; - return depthA - depthB; - }); - }; - this.resortRoom(); - var typesChanged = () => { - this.currentType = -1; - this.resortRoom(); - }; - window.signals.on('typesChanged', typesChanged); - this.on('unmount', () => { - window.signals.off('typesChanged', typesChanged); - }); - /** Canvas redrawing, with all the backgrounds, tiles and copies */ - this.refreshRoomCanvas = () => { - if (this.forbidDrawing) {return;} - let canvas = this.refs.canvas, - sizes = this.refs.canvaswrap.getBoundingClientRect(); - // Перед рисовкой проверим, нормального ли размера наш холст - if (canvas.width != sizes.width || canvas.height != sizes.height) { - canvas.width = sizes.width; - canvas.height = sizes.height; - } - - // Сбросим базовые настройки рисования - canvas.x.setTransform(1,0,0,1,0,0); - canvas.x.globalAlpha = 1; - // Очистим холст - canvas.x.clearRect(0,0,canvas.width,canvas.height); - - // Выполним перемещение с учётом зума - canvas.x.translate(~~(canvas.width / 2), ~~(canvas.height / 2)); - canvas.x.scale(this.zoomFactor,this.zoomFactor); - canvas.x.translate(-this.roomx, -this.roomy); - canvas.x.imageSmoothingEnabled = !global.currentProject.settings.pixelatedrender; - - if (this.stack.length > 0) { // есть слои вообще? - // копии - for (let i = 0, li = this.stack.length; i < li; i++) { - if (this.stack[i].tiles) { // это слой с тайлами - let layer = this.stack[i]; - if (!layer.hidden) { - for (let tile of layer.tiles) { - let w, h, x, y, - img = glob.texturemap[tile.texture], - texture = img.g; - x = texture.offx + (texture.width + texture.marginx) * tile.grid[0] - texture.marginx; - y = texture.offy + (texture.height + texture.marginy) * tile.grid[1] - texture.marginy; - w = (texture.width + texture.marginx) * tile.grid[2] - texture.marginx; - h = (texture.height + texture.marginy) * tile.grid[3] - texture.marginy; - canvas.x.drawImage( - img, - x, y, w, h, - tile.x, tile.y, w, h - ); - } - } - } else if (this.stack[i].texture) { // это слой-фон - if (this.stack[i].texture !== -1) { - if (!('extends' in this.stack[i])) { - this.stack[i].extends = {}; - } - let scx = this.stack[i].extends.scaleX || 1, - scy = this.stack[i].extends.scaleY || 1, - shx = this.stack[i].extends.shiftX || 0, - shy = this.stack[i].extends.shiftY || 0; - canvas.x.save(); - canvas.x.fillStyle = canvas.x.createPattern(glob.texturemap[this.stack[i].texture], this.stack[i].extends.repeat || 'repeat'); - canvas.x.translate(shx, shy); - canvas.x.scale(scx, scy); - canvas.x.fillRect( - (this.xToRoom(0) - shx) / scx, (this.yToRoom(0) - shy) / scy, - canvas.width / scx / this.zoomFactor, - canvas.height / scy / this.zoomFactor - ); - canvas.x.restore(); - } - } else { // Это копия - let copy = this.stack[i], - type = global.currentProject.types[glob.typemap[copy.uid]]; - let texture, gra, w, h, ox, oy, - grax, gray; // Центр рисовки графики - if (type.texture != -1) { - texture = glob.texturemap[type.texture]; - gra = glob.texturemap[type.texture].g; - w = gra.width; - h = gra.height; - ox = gra.offx; - oy = gra.offy; - grax = gra.axis[0]; - gray = gra.axis[1]; - } else { - texture = glob.texturemap[-1]; - w = h = 32; - grax = gray = 16; - ox = oy = 0; - } - if (copy.tx || copy.ty || copy.tr) { - canvas.x.save(); - canvas.x.translate(copy.x, copy.y); - canvas.x.rotate((copy.tr || 0) * Math.PI / -180); - canvas.x.scale(copy.tx || 1, copy.ty || 1); - canvas.x.drawImage( - texture, - ox, oy, w, h, - -grax * (copy.tx || 1), -gray * (copy.ty || 1), w, h - ); - canvas.x.restore(); - } else { - canvas.x.drawImage( - texture, - glob.texturemap[type.texture].g.offx, glob.texturemap[type.texture].g.offy, w, h, - copy.x - grax, copy.y - gray, w, h - ); - } - } - } - } - - // Это рисовка сетки - if (this.room.gridX > 1) { - canvas.x.globalCompositeOperation = 'exclusion'; - canvas.x.fillStyle = canvas.x.createPattern(this.gridCanvas, 'repeat'); - canvas.x.fillRect( - this.xToRoom(0), this.yToRoom(0), - canvas.width / this.zoomFactor, canvas.height / this.zoomFactor); - canvas.x.globalCompositeOperation = 'source-over'; - } - - // Обводка выделенных тайлов - if (this.tab === 'roomtiles' && this.selectedTiles && this.selectedTiles.length) { - for (const tile of this.selectedTiles) { - let g = glob.texturemap[tile.texture].g; - this.drawSelection(tile.x, tile.y, tile.x + g.width*tile.grid[2], tile.y + g.height*tile.grid[3]); - } - } - // Обводка выделенных копий - if (this.tab === 'roomcopies' && this.selectedCopies && this.selectedCopies.length) { - for (const copy of this.selectedCopies) { - this.drawSelection(copy); - } - } - - // Обводка границ комнаты - this.drawSelection(-1.5, -1.5, this.room.width+1.5, this.room.height+1.5); - }; - this.drawSelection = (x1, y1, x2, y2) => { - const cx = this.refs.canvas.x; - cx.lineJoin = 'round'; - cx.lineCap = 'round'; - if (typeof x1 !== 'number') { - const copy = x1, - type = global.currentProject.types[glob.typemap[copy.uid]], - texture = glob.texturemap[type.texture].g; - var left, top, height, width; - if (copy.tr) { - cx.strokeStyle = localStorage.UItheme === 'Night'? '#44dbb5' : '#446adb'; - cx.lineWidth = 3; - cx.beginPath(); - cx.moveTo(copy.x - 32, copy.y); - cx.lineTo(copy.x + 32, copy.y); - cx.moveTo(copy.x, copy.y - 32); - cx.lineTo(copy.x, copy.y + 32); - cx.stroke(); - cx.strokeStyle = localStorage.UItheme === 'Night'? '#1C2B42' : '#fff'; - cx.lineWidth = 1; - cx.moveTo(copy.x - 32, copy.y); - cx.lineTo(copy.x + 32, copy.y); - cx.moveTo(copy.x, copy.y - 32); - cx.lineTo(copy.x, copy.y + 32); - cx.stroke(); - return; - } - if (type.texture !== -1) { - left = copy.x - texture.axis[0] * (copy.tx || 1) - 1.5; - top = copy.y - texture.axis[1] * (copy.ty || 1) - 1.5; - width = texture.width * (copy.tx || 1) + 3; - height = texture.height * (copy.ty || 1) + 3; - } else { - left = copy.x - 16 - 1.5; - top = copy.y - 16 - 1.5; - height = 32 + 3; - width = 32 + 3; - } - x1 = left; - y1 = top; - x2 = left + width; - y2 = top + height; - } - cx.strokeStyle = localStorage.UItheme === 'Night'? '#44dbb5' : '#446adb'; - cx.lineWidth = 3; - cx.strokeRect(x1, y1, x2-x1, y2-y1); - cx.strokeStyle = localStorage.UItheme === 'Night'? '#1C2B42' : '#fff'; - cx.lineWidth = 1; - cx.strokeRect(x1, y1, x2-x1, y2-y1); - }; - - /** - * Генерирует миниатюру комнаты - */ - this.roomGenSplash = function() { - return new Promise((accept, decline) => { - var c = document.createElement('canvas'), - w, h, k; - c.x = c.getContext('2d'); - c.width = 340; - c.height = 256; - c.x.clearRect(0, 0, c.width, c.height); - w = this.refs.canvas.width; - h = this.refs.canvas.height; - if (w / c.width > h / c.height) { - k = c.width / w; - } else { - k = c.height / h; - } - if (k > 1) k = 1; - c.x.drawImage( - this.refs.canvas, - 0, 0, this.refs.canvas.width, this.refs.canvas.height, - (c.width - this.refs.canvas.width*k)/2, (c.height - this.refs.canvas.height*k)/2, - this.refs.canvas.width*k, - this.refs.canvas.height*k - ); - var data = c.toDataURL().replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); - var nam = global.projdir + '/img/r' + this.room.thumbnail + '.png'; - fs.writeFile(nam, buf, function(err) { - if (err) { - decline(err); - } else { - accept(nam); - } - }); - var nam2 = global.projdir + '/img/splash.png'; - fs.writeFile(nam2, buf, function(err) { - if (err) { - decline(err); - } - }); - }); - }; +room-editor.panel.view + .toolbar.tall(style="width: {sidebarWidth}px") + .settings.nogrow.noshrink + b {voc.name} + br + input.wide(type="text" value="{room.name}" onchange="{wire('this.room.name')}") + .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} + .fifty.npt.npb.npl + b {voc.width} + br + input.wide(type="number" value="{room.width}" onchange="{wire('this.room.width')}") + .fifty.npt.npb.npr + b {voc.height} + br + input.wide(type="number" value="{room.height}" onchange="{wire('this.room.height')}") + br + button.wide(onclick="{openRoomEvents}") + svg.feather(if="{room.oncreate || room.onstep || room.ondestroy || room.ondraw}") + use(xlink:href="data/icons.svg#check") + span {voc.events} + .palette + .tabwrap + ul.tabs.nav.noshrink.nogrow + li(onclick="{changeTab('roomcopies')}" class="{active: tab === 'roomcopies'}") {voc.copies} + li(onclick="{changeTab('roombackgrounds')}" class="{active: tab === 'roombackgrounds'}") {voc.backgrounds} + li(onclick="{changeTab('roomtiles')}" class="{active: tab === 'roomtiles'}") {voc.tiles} + .relative + room-type-picker(show="{tab === 'roomcopies'}" current="{currentType}") + room-backgrounds-editor(show="{tab === 'roombackgrounds'}" room="{room}") + room-tile-editor(show="{tab === 'roomtiles'}" room="{room}") + .done.nogrow + button.wide#roomviewdone(onclick="{roomSave}") + svg.feather + use(xlink:href="data/icons.svg#check") + span {voc.done} + .aResizer.vertical(ref="gutter" onmousedown="{gutterMouseDown}") + .editor(ref="canvaswrap") + canvas( + ref="canvas" + onclick="{onCanvasClick}" + onmousedown="{onCanvasPress}" + onmousemove="{onCanvasMove}" + onmouseup="{onCanvasMouseUp}" + onmouseout="{refreshRoomCanvas}" + onmousewheel="{onCanvasWheel}" + oncontextmenu="{onCanvasContextMenu}" + ) + .shift + button.inline.square(title="{voc.shift}" onclick="{roomShift}") + svg.feather + use(xlink:href="data/icons.svg#move") + span(if="{window.innerWidth - sidebarWidth > 840}") {voc.hotkeysNotice} + .zoom + b(if="{window.innerWidth - sidebarWidth > 840}") {vocGlob.zoom} + div.button-stack + button#roomzoom12.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(0.125)}" class="{active: zoomFactor === 0.125}") 12% + button#roomzoom25.inline(onclick="{roomToggleZoom(0.25)}" class="{active: zoomFactor === 0.25}") 25% + button#roomzoom50.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(0.5)}" class="{active: zoomFactor === 0.5}") 50% + button#roomzoom100.inline(onclick="{roomToggleZoom(1)}" class="{active: zoomFactor === 1}") 100% + button#roomzoom200.inline(onclick="{roomToggleZoom(2)}" class="{active: zoomFactor === 2}") 200% + button#roomzoom400.inline(if="{window.innerWidth - sidebarWidth > 470}" onclick="{roomToggleZoom(4)}" class="{active: zoomFactor === 4}") 400% + .grid + button#roomgrid(onclick="{roomToggleGrid}" class="{active: room.gridX > 0}") + span {voc[room.gridX > 0? 'gridoff' : 'grid']} + .center + button#roomcenter(onclick="{roomToCenter}") {voc.tocenter} + span.aMouseCoord(if="{window.innerWidth - sidebarWidth > 470}" ref="mousecoords") ({mouseX}:{mouseY}) + room-events-editor(if="{editingCode}" room="{room}") + context-menu(menu="{roomCanvasCopiesMenu}" ref="roomCanvasCopiesMenu") + context-menu(menu="{roomCanvasMenu}" ref="roomCanvasMenu") + context-menu(menu="{roomCanvasTileMenu}" ref="roomCanvasTileMenu") + context-menu(menu="{roomCanvasTilesMenu}" ref="roomCanvasTilesMenu") + script. + const minSizeW = 250; + const getMaxSizeW = () => window.innerWidth - 300; + this.sidebarWidth = Math.max( + minSizeW, + Math.min(getMaxSizeW(), localStorage.roomSidebarWidth || 300) + ); + + this.gutterMouseDown = () => { + this.draggingGutter = true; + }; + const gutterMove = e => { + if (!this.draggingGutter) { + return; + } + this.sidebarWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), e.clientX)); + localStorage.roomSidebarWidth = this.sidebarWidth; + this.update(); + var {canvas} = this.refs, + sizes = this.refs.canvaswrap.getBoundingClientRect(); + if (canvas.width !== sizes.width || canvas.height !== sizes.height) { + canvas.width = sizes.width; + canvas.height = sizes.height; + } + this.refreshRoomCanvas(); + }; + const gutterUp = () => { + if (this.draggingGutter) { + this.draggingGutter = false; + // updateCanvasSize(); + // document.body.removeChild(catcher); + } + }; + document.addEventListener('mousemove', gutterMove); + document.addEventListener('mouseup', gutterUp); + this.on('unmount', () => { + document.removeEventListener('mousemove', gutterMove); + document.removeEventListener('mouseup', gutterUp); + }); + + this.editingCode = false; + this.forbidDrawing = false; + const fs = require('fs-extra'); + const glob = require('./data/node_requires/glob'); + this.namespace = 'roomview'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.mixin(window.roomCopyTools); + this.mixin(window.roomTileTools); + + this.room = this.opts.room; + + this.mouseX = this.mouseY = 0; + this.roomx = this.room.width / 2; + this.roomy = this.room.height / 2; + this.zoomFactor = 1; + this.room.gridX = this.room.gridX || this.room.grid || 64; + this.room.gridY = this.room.gridY || this.room.grid || 64; + this.dragging = false; + this.tab = 'roomcopies'; + + var updateCanvasSize = () => { + // Firstly, check that we don't need to reflow the layout due to window shrinking + const oldSidebarWidth = this.sidebarWidth; + this.sidebarWidth = Math.max(minSizeW, Math.min(getMaxSizeW(), this.sidebarWidth)); + if (oldSidebarWidth !== this.sidebarWidth) { + this.update(); + } + var {canvas} = this.refs, + sizes = this.refs.canvaswrap.getBoundingClientRect(); + if (canvas.width !== sizes.width || canvas.height !== sizes.height) { + canvas.width = sizes.width; + canvas.height = sizes.height; + } + setTimeout(this.refreshRoomCanvas, 10); + }; + this.on('update', () => { + if (global.currentProject.rooms.find(room => + this.room.name === room.name && this.room !== room)) { + this.nameTaken = true; + } else { + this.nameTaken = false; + } + }); + this.on('mount', () => { + this.room = this.opts.room; + this.refs.canvas.x = this.refs.canvas.getContext('2d'); + this.gridCanvas = document.createElement('canvas'); + this.gridCanvas.x = this.gridCanvas.getContext('2d'); + this.redrawGrid(); + window.addEventListener('resize', updateCanvasSize); + updateCanvasSize(); + }); + this.on('unmount', () => { + window.removeEventListener('resize', updateCanvasSize); + }); + + this.openRoomEvents = () => { + this.editingCode = true; + }; + + // Навигация по комнате, настройки вида + this.roomToggleZoom = zoomFactor => () => { + this.zoomFactor = zoomFactor; + this.redrawGrid(); + this.refreshRoomCanvas(); + }; + this.roomToCenter = () => { + this.roomx = this.room.width / 2; + this.roomy = this.room.height / 2; + this.refreshRoomCanvas(); + }; + this.redrawGrid = () => { + this.gridCanvas.width = this.room.gridX; + this.gridCanvas.height = this.room.gridY; + this.gridCanvas.x.clearRect(0, 0, this.room.gridX, this.room.gridY); + this.gridCanvas.x.globalAlpha = 0.3; + this.gridCanvas.x.strokeStyle = localStorage.UItheme === 'Night' ? '#44dbb5' : '#446adb'; + this.gridCanvas.x.lineWidth = 1 / this.zoomFactor; + this.gridCanvas.x.strokeRect( + 0.5 / this.zoomFactor, 0.5 / this.zoomFactor, + this.room.gridX, this.room.gridY + ); + }; + this.roomToggleGrid = () => { + if (this.room.gridX === 0) { + window.alertify + .confirm(this.voc.gridsize + '
x ') + .then(e => { + if (e.buttonClicked === 'ok') { + this.room.gridX = Number(document.getElementById('theGridSizeX').value); + this.room.gridY = Number(document.getElementById('theGridSizeY').value); + } + this.redrawGrid(); + this.refreshRoomCanvas(); + this.update(); + }); + } else { + this.refreshRoomCanvas(); + this.room.gridX = 0; + this.room.gridY = 0; + } + }; + + // Работа с копиями + this.tab = 'roomcopies'; + this.changeTab = tab => () => { + this.tab = tab; + if (tab === 'roombackgrounds') { + this.roomUnpickType(); + } + }; + this.roomUnpickType = () => { + this.currentType = -1; + }; + + /** Преобразовать x на канвасе в x на комнате */ + this.xToRoom = x => (x - Math.floor(this.refs.canvas.width / 2)) / this.zoomFactor + this.roomx; + /** Преобразовать y на канвасе в y на комнате */ + this.yToRoom = y => (y - Math.floor(this.refs.canvas.height / 2)) / this.zoomFactor + this.roomy; + /** Преобразовать x в комнате в x на канвасе */ + this.xToCanvas = x => (x - this.roomx) * this.zoomFactor + Math.floor(this.refs.canvas.width / 2); + /** Преобразовать y в комнате в y на канвасе */ + this.yToCanvas = y => (y - this.roomy) * this.zoomFactor + Math.floor(this.refs.canvas.height / 2); + + this.onCanvasClick = e => { + if (this.tab === 'roomcopies') { + this.onCanvasClickCopies(e); + } else if (this.tab === 'roomtiles') { + this.onCanvasClickTiles(e); + } + }; + /** При нажатии на канвас, если не выбрана копия, то начинаем перемещение */ + this.onCanvasPress = e => { + this.mouseDown = true; + this.startx = e.offsetX; + this.starty = e.offsetY; + + if (this.tab === 'roomcopies' && this.onCanvasPressCopies(e)) { + return; + } + if ((this.currentType === -1 && !e.shiftKey && this.tab !== 'roomtiles' && e.button === 0 && !e.ctrlKey) || + e.button === 1) { + this.dragging = true; + } + }; + /** и безусловно прекращаем перемещение при отпускании мыши */ + this.onCanvasMouseUp = e => { + this.mouseDown = false; + this.lastCopyX = null; + this.lastCopyY = null; + this.lastTileX = null; + this.lastTileY = null; + if (this.dragging) { + this.dragging = false; + } else if (this.tab === 'roomtiles') { + this.onCanvasMouseUpTiles(e); + } else if (this.tab === 'roomcopies') { + this.onCanvasMouseUpCopies(e); + } + setTimeout(() => { + this.movingStuff = false; + }, 0); + }; + this.drawDeleteCircle = e => { + // Рисовка кружка для удаления копий + var maxdist = Math.max(this.room.gridX, this.room.gridY); + this.refreshRoomCanvas(e); + var cx = this.refs.canvas.x; + cx.fillStyle = '#F00'; + cx.strokeStyle = '#000'; + cx.globalAlpha = 0.5; + cx.beginPath(); + cx.arc(this.xToRoom(e.offsetX), this.yToRoom(e.offsetY), maxdist, 0, 2 * Math.PI); + cx.fill(); + cx.stroke(); + }; + + /** + * Updating mouse coordinates display at the bottom-left corner + */ + this.updateMouseCoords = function updateMouseCoords(e) { + var dx = Math.floor(this.xToRoom(e.offsetX)), + dy = Math.floor(this.yToRoom(e.offsetY)); + if (this.room.gridX === 0 || e.altKey) { + this.mouseX = dx; + this.mouseY = dy; + } else { + this.mouseX = Math.round(dx / this.room.gridX) * this.room.gridX; + this.mouseY = Math.round(dy / this.room.gridY) * this.room.gridY; + } + this.refs.mousecoords.innerHTML = `(${this.mouseX}:${this.mouseY})`; + }; + + /** Начинаем перемещение, или же показываем предварительное расположение новой копии */ + this.onCanvasMove = e => { + e.preventUpdate = true; + if (this.dragging && !this.movingStuff) { + // перетаскивание + this.roomx -= Math.floor(e.movementX / this.zoomFactor); + this.roomy -= Math.floor(e.movementY / this.zoomFactor); + this.refreshRoomCanvas(e); + } else if ( // если зажата мышь и клавиша Shift, то создавать больше копий/тайлов + e.shiftKey && this.mouseDown && + ( + (this.tab === 'roomcopies' && this.currentType !== -1) || + this.tab === 'roomtiles' + ) + ) { + this.onCanvasClick(e); + } else if (this.tab === 'roomcopies') { + this.onCanvasMoveCopies(e); + } else if (this.tab === 'roomtiles') { + this.onCanvasMoveTiles(e); + } + this.updateMouseCoords(e); + }; + + /** При прокрутке колёсиком меняем фактор зума */ + this.onCanvasWheel = e => { + if (e.wheelDelta > 0) { + // in + if (this.zoomFactor === 2) { + this.zoomFactor = 4; + } else if (this.zoomFactor === 1) { + this.zoomFactor = 2; + } else if (this.zoomFactor === 0.5) { + this.zoomFactor = 1; + } else if (this.zoomFactor === 0.25) { + this.zoomFactor = 0.5; + } else if (this.zoomFactor === 0.125) { + this.zoomFactor = 0.25; + } + } else if (this.zoomFactor === 4) { + this.zoomFactor = 2; + } else if (this.zoomFactor === 2) { + this.zoomFactor = 1; + } else if (this.zoomFactor === 1) { + this.zoomFactor = 0.5; + } else if (this.zoomFactor === 0.5) { + this.zoomFactor = 0.25; + } else if (this.zoomFactor === 0.25) { + this.zoomFactor = 0.125; + } + this.redrawGrid(); + this.refreshRoomCanvas(e); + this.updateMouseCoords(e); + }; + this.onCanvasContextMenu = e => { + this.dragging = false; + this.mouseDown = false; + if (this.tab === 'roomcopies') { + if (this.selectedCopies && this.selectedCopies.length) { + this.onCanvasContextMenuMultipleCopies(e); + } else { + this.onCanvasContextMenuCopies(e); + } + } else if (this.tab === 'roomtiles') { + if (this.selectedTiles && this.selectedTiles.length) { + this.onCanvasContextMenuMultipleTiles(e); + } else { + this.onCanvasContextMenuTiles(e); + } + } + e.preventDefault(); + return true; + }; + + // Shifts all the copies in a room at once. + this.roomShift = () => { + window.alertify.confirm(` + ${window.languageJSON.roomview.shifttext} + + + `) + .then(e => { + if (e.buttonClicked === 'ok') { + var dx = Number(document.getElementById('roomshiftx').value) || 0, + dy = Number(document.getElementById('roomshifty').value) || 0; + for (const copy of this.room.copies) { + copy.x += dx; + copy.y += dy; + } + for (const tileLayer of this.room.tiles) { + for (const tile of tileLayer.tiles) { + tile.x += dx; + tile.y += dy; + } + } + this.refreshRoomCanvas(); + } + }); + }; + + /** Saves a room (in fact, just marks a project as an unsaved, and closes the room editor) */ + this.roomSave = () => { + if (this.nameTaken) { + // animate the error notice + require('./data/node_requires/jellify')(this.refs.errorNotice); + window.soundbox.play('Failure'); + return false; + } + this.room.lastmod = Number(new Date()); + this.roomGenSplash() + .then(() => { + glob.modified = true; + this.parent.editing = false; + this.parent.update(); + }) + .catch(err => { + console.error(err); + glob.modified = true; + this.parent.editing = false; + this.parent.update(); + }); + return true; + }; + + this.resortRoom = () => { + // Make an array of all the backgrounds, tile layers and copies, and then sort it. + this.stack = this.room.copies.concat(this.room.backgrounds).concat(this.room.tiles); + const projTypes = global.currentProject.types; + this.stack.sort((a, b) => { + const depthA = a.depth !== void 0 ? a.depth : projTypes[glob.typemap[a.uid]].depth, + depthB = b.depth !== void 0 ? b.depth : projTypes[glob.typemap[b.uid]].depth; + return depthA - depthB; + }); + }; + this.resortRoom(); + var typesChanged = () => { + this.currentType = -1; + this.resortRoom(); + }; + window.signals.on('typesChanged', typesChanged); + this.on('unmount', () => { + window.signals.off('typesChanged', typesChanged); + }); + /** Canvas redrawing, with all the backgrounds, tiles and copies */ + // eslint-disable-next-line max-lines-per-function, complexity + this.refreshRoomCanvas = () => { + if (this.forbidDrawing) { + return; + } + const {canvas} = this.refs, + sizes = this.refs.canvaswrap.getBoundingClientRect(); + // Make sure the canvas size is of correct width + if (Number(canvas.width) !== sizes.width || Number(canvas.height) !== sizes.height) { + canvas.width = sizes.width; + canvas.height = sizes.height; + } + + // Reset drawing transforms + canvas.x.setTransform(1, 0, 0, 1, 0, 0); + canvas.x.globalAlpha = 1; + // Clear the canvas + canvas.x.clearRect(0, 0, canvas.width, canvas.height); + + // Apply camera movement + zoom + canvas.x.translate(Math.floor(canvas.width / 2), Math.floor(canvas.height / 2)); + canvas.x.scale(this.zoomFactor, this.zoomFactor); + canvas.x.translate(-this.roomx, -this.roomy); + + // Disable pixel interpolation, if needed + canvas.x.imageSmoothingEnabled = !global.currentProject.settings.pixelatedrender; + + for (let i = 0, li = this.stack.length; i < li; i++) { + if (this.stack[i].tiles) { // a tile layer + const layer = this.stack[i]; + if (!layer.hidden) { + for (const tile of layer.tiles) { + const img = glob.texturemap[tile.texture], + tex = img.g; + const x = tex.offx + (tex.width + tex.marginx) * tile.grid[0] - tex.marginx, + y = tex.offy + (tex.height + tex.marginy) * tile.grid[1] - tex.marginy, + w = (tex.width + tex.marginx) * tile.grid[2] - tex.marginx, + h = (tex.height + tex.marginy) * tile.grid[3] - tex.marginy; + canvas.x.drawImage( + img, + x, y, w, h, + tile.x, tile.y, w, h + ); + } + } + } else if (this.stack[i].texture) { // это слой-фон + if (this.stack[i].texture !== -1) { + if (!('extends' in this.stack[i])) { + this.stack[i].extends = {}; + } + const scx = this.stack[i].extends.scaleX || 1, + scy = this.stack[i].extends.scaleY || 1, + shx = this.stack[i].extends.shiftX || 0, + shy = this.stack[i].extends.shiftY || 0; + canvas.x.save(); + canvas.x.fillStyle = canvas.x.createPattern(glob.texturemap[this.stack[i].texture], this.stack[i].extends.repeat || 'repeat'); + canvas.x.translate(shx, shy); + canvas.x.scale(scx, scy); + canvas.x.fillRect( + (this.xToRoom(0) - shx) / scx, (this.yToRoom(0) - shy) / scy, + canvas.width / scx / this.zoomFactor, + canvas.height / scy / this.zoomFactor + ); + canvas.x.restore(); + } + } else { // A copy + const copy = this.stack[i], + type = global.currentProject.types[glob.typemap[copy.uid]]; + let texture, gra, w, h, ox, oy, + grax, gray; // texture's drawing center + if (type.texture !== -1) { + texture = glob.texturemap[type.texture]; + gra = glob.texturemap[type.texture].g; + w = gra.width; + h = gra.height; + ox = gra.offx; + oy = gra.offy; + [grax, gray] = gra.axis; + } else { + texture = glob.texturemap[-1]; + w = h = 32; + grax = gray = 16; + ox = oy = 0; + } + if (copy.tx || copy.ty || copy.tr) { + canvas.x.save(); + canvas.x.translate(copy.x, copy.y); + canvas.x.rotate((copy.tr || 0) * Math.PI / -180); + canvas.x.scale(copy.tx || 1, copy.ty || 1); + canvas.x.drawImage( + texture, + ox, oy, w, h, + -grax * (copy.tx || 1), -gray * (copy.ty || 1), w, h + ); + canvas.x.restore(); + } else { + const tex = glob.texturemap[type.texture].g; + canvas.x.drawImage( + texture, + tex.offx, tex.offy, w, h, + copy.x - grax, copy.y - gray, w, h + ); + } + } + } + + // Grid drawing + if (this.room.gridX > 1) { + canvas.x.globalCompositeOperation = 'exclusion'; + canvas.x.fillStyle = canvas.x.createPattern(this.gridCanvas, 'repeat'); + canvas.x.fillRect( + this.xToRoom(0), this.yToRoom(0), + canvas.width / this.zoomFactor, canvas.height / this.zoomFactor + ); + canvas.x.globalCompositeOperation = 'source-over'; + } + + // Outline selected tiles + if (this.tab === 'roomtiles' && this.selectedTiles && this.selectedTiles.length) { + for (const tile of this.selectedTiles) { + const {g} = glob.texturemap[tile.texture]; + this.drawSelection( + tile.x, + tile.y, + tile.x + g.width * tile.grid[2], + tile.y + g.height * tile.grid[3] + ); + } + } + // Outline selected copies + if (this.tab === 'roomcopies' && this.selectedCopies && this.selectedCopies.length) { + for (const copy of this.selectedCopies) { + this.drawSelection(copy); + } + } + + // Outline the starting viewport frame + this.drawSelection(-1.5, -1.5, this.room.width + 1.5, this.room.height + 1.5); + }; + + this.drawSelection = (x1, y1, x2, y2) => { + const cx = this.refs.canvas.x; + cx.lineJoin = 'round'; + cx.lineCap = 'round'; + if (typeof x1 !== 'number') { + const copy = x1, + type = global.currentProject.types[glob.typemap[copy.uid]], + texture = glob.texturemap[type.texture].g; + var left, top, height, width; + if (copy.tr) { + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#44dbb5' : '#446adb'; + cx.lineWidth = 3; + cx.beginPath(); + cx.moveTo(copy.x - 32, copy.y); + cx.lineTo(copy.x + 32, copy.y); + cx.moveTo(copy.x, copy.y - 32); + cx.lineTo(copy.x, copy.y + 32); + cx.stroke(); + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#1C2B42' : '#fff'; + cx.lineWidth = 1; + cx.moveTo(copy.x - 32, copy.y); + cx.lineTo(copy.x + 32, copy.y); + cx.moveTo(copy.x, copy.y - 32); + cx.lineTo(copy.x, copy.y + 32); + cx.stroke(); + return; + } + if (type.texture !== -1) { + left = copy.x - texture.axis[0] * (copy.tx || 1) - 1.5; + top = copy.y - texture.axis[1] * (copy.ty || 1) - 1.5; + width = texture.width * (copy.tx || 1) + 3; + height = texture.height * (copy.ty || 1) + 3; + } else { + left = copy.x - 16 - 1.5; + top = copy.y - 16 - 1.5; + height = 32 + 3; + width = 32 + 3; + } + x1 = left; + y1 = top; + x2 = left + width; + y2 = top + height; + } + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#44dbb5' : '#446adb'; + cx.lineWidth = 3; + cx.strokeRect(x1, y1, x2 - x1, y2 - y1); + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#1C2B42' : '#fff'; + cx.lineWidth = 1; + cx.strokeRect(x1, y1, x2 - x1, y2 - y1); + }; + + /** + * Генерирует миниатюру комнаты + */ + this.roomGenSplash = function roomGenSplash() { + return new Promise((accept, decline) => { + var c = document.createElement('canvas'), + w, h, k; + c.x = c.getContext('2d'); + c.width = 340; + c.height = 256; + c.x.clearRect(0, 0, c.width, c.height); + w = this.refs.canvas.width; + h = this.refs.canvas.height; + if (w / c.width > h / c.height) { + k = c.width / w; + } else { + k = c.height / h; + } + if (k > 1) { + k = 1; + } + c.x.drawImage( + this.refs.canvas, + 0, 0, this.refs.canvas.width, this.refs.canvas.height, + (c.width - this.refs.canvas.width * k) / 2, + (c.height - this.refs.canvas.height * k) / 2, + this.refs.canvas.width * k, + this.refs.canvas.height * k + ); + var splashBase64 = c.toDataURL().replace(/^data:image\/\w+;base64,/, ''); + var buf = new Buffer(splashBase64, 'base64'); + var nam = global.projdir + '/img/r' + this.room.thumbnail + '.png'; + fs.writeFile(nam, buf, function roomGenSplashCallback(err) { + if (err) { + decline(err); + } else { + accept(nam); + } + }); + var nam2 = global.projdir + '/img/splash.png'; + fs.writeFile(nam2, buf, function projGenSplashCallback(err) { + if (err) { + decline(err); + } + }); + }); + }; diff --git a/src/riotTags/rooms/room-events-editor.tag b/src/riotTags/rooms/room-events-editor.tag index 8ea0d0fc9..0b0e5a4cf 100644 --- a/src/riotTags/rooms/room-events-editor.tag +++ b/src/riotTags/rooms/room-events-editor.tag @@ -1,133 +1,136 @@ -room-events-editor.view.panel - .tabwrap - ul.tabs.nav.nogrow.noshrink.nb - li(onclick="{switchTab('roomcreate')}" class="{active: tab === 'roomcreate'}" title="Control-Q" data-hotkey="Control+q") - svg.feather - use(xlink:href="data/icons.svg#sun") - span {voc.create} - li(onclick="{switchTab('roomstep')}" class="{active: tab === 'roomstep'}" title="Control-W" data-hotkey="Control+w") - svg.feather - use(xlink:href="data/icons.svg#skip-forward") - span {voc.step} - li(onclick="{switchTab('roomdraw')}" class="{active: tab === 'roomdraw'}" title="Control-E" data-hotkey="Control+e") - svg.feather - use(xlink:href="data/icons.svg#edit-2") - span {voc.draw} - li(onclick="{switchTab('roomleave')}" class="{active: tab === 'roomleave'}" title="Control-R" data-hotkey="Control+r") - svg.feather - use(xlink:href="data/icons.svg#trash") - span {voc.leave} - div(style="position: relative;") - .tabbed(show="{tab === 'roomcreate'}") - .aCodeEditor(ref="roomoncreate") - .tabbed(show="{tab === 'roomstep'}") - .aCodeEditor(ref="roomonstep") - .tabbed(show="{tab === 'roomdraw'}") - .aCodeEditor(ref="roomondraw") - .tabbed(show="{tab === 'roomleave'}") - .aCodeEditor(ref="roomonleave") - button.wide.nogrow.noshrink(onclick="{roomSaveEvents}") - svg.feather - use(xlink:href="data/icons.svg#check") - span {voc.done} - script. - this.namespace = 'roomview'; - this.mixin(window.riotVoc); - - this.tab = 'roomcreate'; - const tabToEditor = tab => { - tab = tab || this.tab; - if (tab === 'roomcreate') { - return this.roomoncreate; - } else if (tab === 'roomstep') { - return this.roomonstep; - } else if (tab === 'roomdraw') { - return this.roomondraw; - } else if (tab === 'roomleave') { - return this.roomonleave; - } - return null; - }; - this.switchTab = tab => e => { - this.tab = tab; - const editor = tabToEditor(tab); - setTimeout(() => { - editor.layout(); - editor.focus(); - }, 0); - }; - - const updateEditorSize = () => { - if (tabToEditor()) { - tabToEditor().layout(); - } - }; - const updateEditorSizeDeferred = function () { - setTimeout(updateEditorSize, 0); - }; - window.signals.on('roomsFocus', this.focusEditor); - window.signals.on('roomsFocus', updateEditorSizeDeferred); - window.addEventListener('resize', updateEditorSize); - this.on('unmount', () => { - window.signals.off('roomsFocus', this.focusEditor); - window.signals.off('roomsFocus', updateEditorSizeDeferred); - window.removeEventListener('resize', updateEditorSize); - }); - this.on('mount', e => { - this.room = this.opts.room; - setTimeout(() => { - var editorOptions = { - language: 'typescript', - lockWrapper: true - }; - this.roomoncreate = window.setupCodeEditor( - this.refs.roomoncreate, - Object.assign({}, editorOptions, { - value: this.room.oncreate, - wrapper: ['function onCreate(this: Room) {', '}'] - }) - ); - this.roomonstep = window.setupCodeEditor(this.refs.roomonstep, - Object.assign({}, editorOptions, { - value: this.room.onstep, - wrapper: ['function onStep(this: Room) {', '}'] - }) - ); - this.roomondraw = window.setupCodeEditor(this.refs.roomondraw, - Object.assign({}, editorOptions, { - value: this.room.ondraw, - wrapper: ['function onDraw(this: Room) {', '}'] - }) - ); - this.roomonleave = window.setupCodeEditor(this.refs.roomonleave, - Object.assign({}, editorOptions, { - value: this.room.onleave, - wrapper: ['function onLeave(this: Room) {', '}'] - }) - ); - this.roomoncreate.onDidChangeModelContent(e => { - this.room.oncreate = this.roomoncreate.getPureValue(); - }); - this.roomonstep.onDidChangeModelContent(e => { - this.room.onstep = this.roomonstep.getPureValue(); - }); - this.roomondraw.onDidChangeModelContent(e => { - this.room.ondraw = this.roomondraw.getPureValue(); - }); - this.roomonleave.onDidChangeModelContent(e => { - this.room.onleave = this.roomonleave.getPureValue(); - }); - }, 0); - }); - this.on('unmount', () => { - // Manually destroy editors to free memory - this.roomoncreate.dispose(); - this.roomonstep.dispose(); - this.roomondraw.dispose(); - this.roomonleave.dispose(); - }); - - this.roomSaveEvents = e => { - this.parent.editingCode = false; - this.parent.update(); - }; +room-events-editor.view.panel + .tabwrap + ul.tabs.nav.nogrow.noshrink.nb + li(onclick="{switchTab('roomcreate')}" class="{active: tab === 'roomcreate'}" title="Control-Q" data-hotkey="Control+q") + svg.feather + use(xlink:href="data/icons.svg#sun") + span {voc.create} + li(onclick="{switchTab('roomstep')}" class="{active: tab === 'roomstep'}" title="Control-W" data-hotkey="Control+w") + svg.feather + use(xlink:href="data/icons.svg#skip-forward") + span {voc.step} + li(onclick="{switchTab('roomdraw')}" class="{active: tab === 'roomdraw'}" title="Control-E" data-hotkey="Control+e") + svg.feather + use(xlink:href="data/icons.svg#edit-2") + span {voc.draw} + li(onclick="{switchTab('roomleave')}" class="{active: tab === 'roomleave'}" title="Control-R" data-hotkey="Control+r") + svg.feather + use(xlink:href="data/icons.svg#trash") + span {voc.leave} + div(style="position: relative;") + .tabbed(show="{tab === 'roomcreate'}") + .aCodeEditor(ref="roomoncreate") + .tabbed(show="{tab === 'roomstep'}") + .aCodeEditor(ref="roomonstep") + .tabbed(show="{tab === 'roomdraw'}") + .aCodeEditor(ref="roomondraw") + .tabbed(show="{tab === 'roomleave'}") + .aCodeEditor(ref="roomonleave") + button.wide.nogrow.noshrink(onclick="{roomSaveEvents}") + svg.feather + use(xlink:href="data/icons.svg#check") + span {voc.done} + script. + this.namespace = 'roomview'; + this.mixin(window.riotVoc); + + this.tab = 'roomcreate'; + const tabToEditor = tab => { + tab = tab || this.tab; + if (tab === 'roomcreate') { + return this.roomoncreate; + } else if (tab === 'roomstep') { + return this.roomonstep; + } else if (tab === 'roomdraw') { + return this.roomondraw; + } else if (tab === 'roomleave') { + return this.roomonleave; + } + return null; + }; + this.switchTab = tab => () => { + this.tab = tab; + const editor = tabToEditor(tab); + setTimeout(() => { + editor.layout(); + editor.focus(); + }, 0); + }; + + const updateEditorSize = () => { + if (tabToEditor()) { + tabToEditor().layout(); + } + }; + const updateEditorSizeDeferred = function () { + setTimeout(updateEditorSize, 0); + }; + window.signals.on('roomsFocus', this.focusEditor); + window.signals.on('roomsFocus', updateEditorSizeDeferred); + window.addEventListener('resize', updateEditorSize); + this.on('unmount', () => { + window.signals.off('roomsFocus', this.focusEditor); + window.signals.off('roomsFocus', updateEditorSizeDeferred); + window.removeEventListener('resize', updateEditorSize); + }); + this.on('mount', () => { + this.room = this.opts.room; + setTimeout(() => { + var editorOptions = { + language: 'typescript', + lockWrapper: true + }; + this.roomoncreate = window.setupCodeEditor( + this.refs.roomoncreate, + Object.assign({}, editorOptions, { + value: this.room.oncreate, + wrapper: ['function onCreate(this: Room) {', '}'] + }) + ); + this.roomonstep = window.setupCodeEditor( + this.refs.roomonstep, + Object.assign({}, editorOptions, { + value: this.room.onstep, + wrapper: ['function onStep(this: Room) {', '}'] + }) + ); + this.roomondraw = window.setupCodeEditor( + this.refs.roomondraw, + Object.assign({}, editorOptions, { + value: this.room.ondraw, + wrapper: ['function onDraw(this: Room) {', '}'] + }) + ); + this.roomonleave = window.setupCodeEditor( + this.refs.roomonleave, + Object.assign({}, editorOptions, { + value: this.room.onleave, + wrapper: ['function onLeave(this: Room) {', '}'] + }) + ); + this.roomoncreate.onDidChangeModelContent(() => { + this.room.oncreate = this.roomoncreate.getPureValue(); + }); + this.roomonstep.onDidChangeModelContent(() => { + this.room.onstep = this.roomonstep.getPureValue(); + }); + this.roomondraw.onDidChangeModelContent(() => { + this.room.ondraw = this.roomondraw.getPureValue(); + }); + this.roomonleave.onDidChangeModelContent(() => { + this.room.onleave = this.roomonleave.getPureValue(); + }); + }, 0); + }); + this.on('unmount', () => { + // Manually destroy editors to free memory + this.roomoncreate.dispose(); + this.roomonstep.dispose(); + this.roomondraw.dispose(); + this.roomonleave.dispose(); + }); + + this.roomSaveEvents = () => { + this.parent.editingCode = false; + this.parent.update(); + }; diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index 2802c7007..11598f3ac 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -18,7 +18,7 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix span.act(title="{vocGlob.delete}" onclick="{deleteTileLayer}") svg.feather use(xlink:href="data/icons.svg#trash") - span.act(title="{parent.currentTileLayer.hidden? voc.show: voc.hide}" onclick="{toggleTileLayerVisibility}") + span.act(title="{parent.currentTileLayer.hidden? voc.show: voc.hide}" onclick="{toggleTileLayer}") svg.feather use(xlink:href="data/icons.svg#{parent.currentTileLayer.hidden? 'eye' : 'eye-off'}") span.act(title="{voc.moveTileLayer}" onclick="{moveTileLayer}") @@ -39,15 +39,15 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix tiles: [] }]; } - this.parent.currentTileLayer = this.opts.room.tiles[0]; + [this.parent.currentTileLayer] = this.opts.room.tiles; this.parent.currentTileLayerId = 0; this.namespace = 'roomtiles'; this.mixin(window.riotVoc); this.mixin(window.riotWired); - this.deleteTileLayer = e => { - alertify + this.deleteTileLayer = () => { + window.alertify .okBtn(window.languageJSON.common.delete) .cancelBtn(window.languageJSON.common.cancel) .confirm(window.languageJSON.common.confirmDelete.replace('{0}', window.languageJSON.common.tilelayer)) @@ -57,7 +57,7 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix var index = tiles.tiles.indexOf(this.parent.currentTileLayer); tiles.tiles.splice(index, 1); if (tiles.tiles.length) { - this.parent.currentTileLayer = tiles.tiles[0]; + [this.parent.currentTileLayer] = tiles.tiles; this.parent.currentTileLayerId = 0; } else { this.parent.currentTileLayer = false; @@ -65,45 +65,44 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix this.parent.resortRoom(); this.parent.refreshRoomCanvas(); this.update(); - alertify + window.alertify .okBtn(window.languageJSON.common.ok) .cancelBtn(window.languageJSON.common.cancel); } }); }; - this.moveTileLayer = e => { - alertify + this.moveTileLayer = () => { + window.alertify .defaultValue(this.parent.currentTileLayer.depth) .prompt(window.languageJSON.roomview.newdepth) - .then(ee => { - if (ee.inputValue && Number(ee.inputValue)) { - this.parent.currentTileLayer.depth = Number(ee.inputValue); + .then(e => { + if (e.inputValue && Number(e.inputValue)) { + this.parent.currentTileLayer.depth = Number(e.inputValue); this.parent.resortRoom(); this.parent.refreshRoomCanvas(); this.update(); } }); }; - this.addTileLayer = e => { - alertify + this.addTileLayer = () => { + window.alertify .defaultValue(-10) .prompt(window.languageJSON.roomview.newdepth) - .then(ee => { - if (ee.inputValue && Number(ee.inputValue)) { + .then(e => { + if (e.inputValue && Number(e.inputValue)) { var layer = { - depth: Number(ee.inputValue), + depth: Number(e.inputValue), tiles: [] }; this.opts.room.tiles.push(layer); this.parent.currentTileLayer = layer; this.parent.currentTileLayerId = this.opts.room.tiles.length - 1; this.parent.resortRoom(); - console.log(this.parent.currentTileLayerId, this.parent.currentTileLayer, this.opts.room.tiles); this.update(); } }); }; - this.toggleTileLayerVisibility = e => { + this.toggleTileLayer = () => { this.parent.currentTileLayer.hidden = !this.parent.currentTileLayer.hidden; this.parent.refreshRoomCanvas(); }; @@ -111,21 +110,21 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix this.parent.currentTileLayer = this.opts.room.tiles[Number(e.target.value)]; this.parent.currentTileLayerId = Number(e.target.value); }; - this.switchTiledImage = e => { + this.switchTiledImage = () => { this.pickingTileset = true; }; - this.onTilesetCancel = e => { + this.onTilesetCancel = () => { this.pickingTileset = false; this.update(); }; - this.onTilesetSelected = texture => e => { + this.onTilesetSelected = texture => () => { this.parent.currentTileset = texture; this.pickingTileset = false; this.redrawTileset(); this.update(); }; - this.redrawTileset = e => { + this.redrawTileset = () => { const glob = require('./data/node_requires/glob'); var c = this.refs.tiledImage, cx = c.getContext('2d'), @@ -140,48 +139,52 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix cx.globalAlpha = 0.5; cx.globalCompositeOperation = 'exclusion'; for (let i = 0, l = Math.min(g.grid[0] * g.grid[1], g.untill || Infinity); i < l; i++) { - let xx = i % g.grid[0], - yy = Math.floor(i / g.grid[0]), - x = g.offx + xx * (g.marginx + g.width), - y = g.offy + yy * (g.marginy + g.height), - w = g.width, - h = g.height; + const xx = i % g.grid[0], + yy = Math.floor(i / g.grid[0]), + x = g.offx + xx * (g.marginx + g.width), + y = g.offy + yy * (g.marginy + g.height), + w = g.width, + h = g.height; cx.strokeRect(x, y, w, h); } cx.globalCompositeOperation = 'source-over'; cx.globalAlpha = 1; cx.lineJoin = 'round'; - cx.strokeStyle = localStorage.UItheme === 'Night'? '#44dbb5' : '#446adb'; + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#44dbb5' : '#446adb'; cx.lineWidth = 3; - let selX = g.offx + this.parent.tileX*(g.width + g.marginx), - selY = g.offy + this.parent.tileY*(g.height + g.marginy), - selW = g.width * this.parent.tileSpanX + g.marginx * (this.parent.tileSpanX - 1), - selH = g.height * this.parent.tileSpanY + g.marginy * (this.parent.tileSpanY - 1); + const selX = g.offx + this.parent.tileX * (g.width + g.marginx), + selY = g.offy + this.parent.tileY * (g.height + g.marginy), + selW = g.width * this.parent.tileSpanX + g.marginx * (this.parent.tileSpanX - 1), + selH = g.height * this.parent.tileSpanY + g.marginy * (this.parent.tileSpanY - 1); cx.strokeRect(-0.5 + selX, -0.5 + selY, selW + 1, selH + 1); - cx.strokeStyle = localStorage.UItheme === 'Night'? '#1C2B42' : '#fff'; + cx.strokeStyle = localStorage.UItheme === 'Night' ? '#1C2B42' : '#fff'; cx.lineWidth = 1; cx.strokeRect(-0.5 + selX, -0.5 + selY, selW + 1, selH + 1); }; this.startTileSelection = e => { - if (!this.parent.currentTileset) {return;} + if (!this.parent.currentTileset) { + return; + } var g = this.parent.currentTileset; this.parent.tileSpanX = 1; this.parent.tileSpanY = 1; this.selectingTile = true; - this.tileStartX = Math.round((e.layerX - g.offx - g.width*0.5) / (g.width+g.marginx)); + this.tileStartX = Math.round((e.layerX - g.offx - g.width * 0.5) / (g.width + g.marginx)); this.tileStartX = Math.max(0, Math.min(g.grid[0], this.tileStartX)); - this.tileStartY = Math.round((e.layerY - g.offy - g.height*0.5) / (g.height+g.marginy)); + this.tileStartY = Math.round((e.layerY - g.offy - g.height * 0.5) / (g.height + g.marginy)); this.tileStartY = Math.max(0, Math.min(g.grid[1], this.tileStartY)); this.parent.tileX = this.tileStartX; this.parent.tileY = this.tileStartY; this.redrawTileset(); }; this.moveTileSelection = e => { - if (!this.selectingTile) {return;} + if (!this.selectingTile) { + return; + } var g = this.parent.currentTileset; - this.tileEndX = Math.round((e.layerX - g.offx - g.width*0.5) / (g.width+g.marginx)); + this.tileEndX = Math.round((e.layerX - g.offx - g.width * 0.5) / (g.width + g.marginx)); this.tileEndX = Math.max(0, Math.min(g.grid[0], this.tileEndX)); - this.tileEndY = Math.round((e.layerY - g.offy - g.height*0.5) / (g.height+g.marginy)); + this.tileEndY = Math.round((e.layerY - g.offy - g.height * 0.5) / (g.height + g.marginy)); this.tileEndY = Math.max(0, Math.min(g.grid[1], this.tileEndY)); this.parent.tileSpanX = 1 + Math.abs(this.tileStartX - this.tileEndX); this.parent.tileSpanY = 1 + Math.abs(this.tileStartY - this.tileEndY); @@ -190,7 +193,9 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix this.redrawTileset(); }; this.stopTileSelection = e => { - if (!this.selectingTile) {return;} + if (!this.selectingTile) { + return; + } this.moveTileSelection(e); this.selectingTile = false; }; \ No newline at end of file diff --git a/src/riotTags/rooms/room-type-picker.tag b/src/riotTags/rooms/room-type-picker.tag index 68554aa9a..f9e21a4f4 100644 --- a/src/riotTags/rooms/room-type-picker.tag +++ b/src/riotTags/rooms/room-type-picker.tag @@ -64,7 +64,7 @@ room-type-picker.room-editor-TypeSwatches.tabbed.tall }; const Fuse = require('fuse.js'); this.fuseSearch = e => { - var val = (e? e.target.value : this.refs.fusesearch.value).trim(); + var val = (e ? e.target.value : this.refs.fusesearch.value).trim(); if (val) { var fuse = new Fuse(this.types, fuseOptions); this.searchResults = fuse.search(val); @@ -72,7 +72,7 @@ room-type-picker.room-editor-TypeSwatches.tabbed.tall this.searchResults = null; } }; - this.selectType = type => e => { + this.selectType = type => () => { this.parent.currentType = type; this.parent.selectedCopies = false; }; diff --git a/src/riotTags/rooms/rooms-panel.tag b/src/riotTags/rooms/rooms-panel.tag index 7ecbdc9be..219edebf9 100644 --- a/src/riotTags/rooms/rooms-panel.tag +++ b/src/riotTags/rooms/rooms-panel.tag @@ -1,222 +1,219 @@ -rooms-panel.panel.view - .flexfix.tall - .flexfix-header - div - .toright - b {vocGlob.sort} - button.inline.square(onclick="{switchSort('date')}" class="{selected: sort === 'date' && !searchResults}") - svg.feather - use(xlink:href="data/icons.svg#clock") - button.inline.square(onclick="{switchSort('name')}" class="{selected: sort === 'name' && !searchResults}") - svg.feather - use(xlink:href="data/icons.svg#sort-alphabetically") - .aSearchWrap - input.inline(type="text" onkeyup="{fuseSearch}") - svg.feather - use(xlink:href="data/icons.svg#search") - button.inline.square(onclick="{switchLayout}") - svg.feather - use(xlink:href="data/icons.svg#{localStorage.roomsLayout === 'list'? 'grid' : 'list'}") - .toleft - button#roomcreate(onclick="{roomCreate}" data-hotkey="Control+n" title="Control+N") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.create} - ul.cards.rooms.flexfix-body(class="{list: localStorage.roomsLayout === 'list'}") - li( - each="{room in (searchResults? searchResults : rooms)}" - class="{starting: global.currentProject.startroom === room.uid}" - onclick="{openRoom(room)}" - oncontextmenu="{menuPopup(room)}" - onlong-press="{menuPopup(room)}" - ) - img(src="file://{global.projdir + '/img/r' + room.thumbnail + '.png?' + room.lastmod}") - span {room.name} - span.date(if="{room.lastmod}") {niceTime(room.lastmod)} - svg.feather(if="{global.currentProject.startroom === room.uid}") - use(xlink:href="data/icons.svg#play") - room-editor(if="{editing}" room="{editingRoom}") - context-menu(menu="{roomMenu}" ref="roomMenu") - script. - const generateGUID = require('./data/node_requires/generateGUID'); - - this.namespace = 'rooms'; - this.mixin(window.riotVoc); - this.mixin(window.riotNiceTime); - this.editing = false; - this.sort = 'name'; - this.sortReverse = false; - - this.updateList = () => { - this.rooms = [...global.currentProject.rooms]; - if (this.sort === 'name') { - this.rooms.sort((a, b) => { - return a.name.localeCompare(b.name); - }); - } else { - this.rooms.sort((a, b) => { - return b.lastmod - a.lastmod; - }); - } - if (this.sortReverse) { - this.rooms.reverse(); - } - }; - this.switchSort = sort => e => { - if (this.sort === sort) { - this.sortReverse = !this.sortReverse; - } else { - this.sort = sort; - this.sortReverse = false; - } - this.updateList(); - }; - this.switchLayout = e => { - localStorage.roomsLayout = localStorage.roomsLayout === 'list'? 'grid' : 'list'; - }; - const fuseOptions = { - shouldSort: true, - tokenize: true, - threshold: 0.5, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: ['name'] - }; - const Fuse = require('fuse.js'); - this.fuseSearch = e => { - if (e.target.value.trim()) { - var fuse = new Fuse(this.rooms, fuseOptions); - this.searchResults = fuse.search(e.target.value.trim()); - } else { - this.searchResults = null; - } - }; - this.setUpPanel = e => { - this.updateList(); - this.searchResults = null; - this.editing = false; - this.editingRoom = null; - this.update(); - }; - window.signals.on('projectLoaded', this.setUpPanel); - this.on('mount', this.setUpPanel); - this.on('unmount', () => { - window.signals.off('projectLoaded', this.setUpPanel); - }); - - const fs = require('fs-extra'), - path = require('path'); - this.roomCreate = function (e) { - if (this.editing) { - return false; - } - var guid = generateGUID(), - thumbnail = guid.split('-').pop(); - fs.copy('./data/img/notexture.png', path.join(global.projdir, '/img/r' + thumbnail + '.png'), () => { - var newRoom = { - name: 'Room_' + thumbnail, - oncreate: '', - onstep: '', - ondraw: '', - onleave: '', - width: 800, - height: 600, - backgrounds: [], - copies: [], - tiles: [], - uid: guid, - thumbnail: thumbnail - }; - global.currentProject.rooms.push(newRoom); - this.openRoom(newRoom)(); - this.updateList(); - this.update(); - }); - }; - this.openRoom = room => e => { - this.editingRoom = room; - this.editing = true; - }; - - this.roomMenu = { - items: [{ - label: this.voc.makestarting, - click: () => { - global.currentProject.startroom = this.editingRoom.uid; - this.update(); - } - }, { - label: window.languageJSON.common.open, - click: () => { - this.openRoom(this.editingRoom)(); - this.update(); - } - }, { - label: languageJSON.common.copyName, - click: e => { - nw.Clipboard.get().set(this.editingRoom.name, 'text'); - } - }, { - label: window.languageJSON.common.duplicate, - click: () => { - alertify - .defaultValue(this.editingRoom.name + '_dup') - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue != '' && e.buttonClicked !== 'cancel') { - var guid = generateGUID(), - thumbnail = guid.split('-').pop(); - var newRoom = JSON.parse(JSON.stringify(this.editingRoom)); - newRoom.name = e.inputValue; - global.currentProject.rooms.push(newRoom); - newRoom.uid = guid; - newRoom.thumbnail = thumbnail; - fs.linkSync(global.projdir + '/img/r' + this.editingRoom.thumbnail + '.png', global.projdir + '/img/r' + thumbnail + '.png') - this.updateList(); - this.update(); - } - }); - } - }, { - label: window.languageJSON.common.rename, - click: () => { - alertify - .defaultValue(this.editingRoom.name) - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue != '' && e.buttonClicked !== 'cancel') { - var nam = e.inputValue; - this.editingRoom.name = nam; - this.update(); - } - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.common.delete, - click: () => { - alertify - .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editingRoom.name)) - .then(e => { - if (e.buttonClicked === 'ok') { - var ind = global.currentProject.rooms.indexOf(this.editingRoom); - global.currentProject.rooms.splice(ind, 1); - this.updateList(); - this.update(); - alertify - .okBtn(window.languageJSON.common.ok) - .cancelBtn(window.languageJSON.common.cancel); - } - }); - } - }] - }; - - this.menuPopup = room => e => { - this.editingRoom = room; - this.refs.roomMenu.popup(e.clientX, e.clientY); - e.preventDefault(); - }; +rooms-panel.panel.view + .flexfix.tall + .flexfix-header + div + .toright + b {vocGlob.sort} + button.inline.square(onclick="{switchSort('date')}" class="{selected: sort === 'date' && !searchResults}") + svg.feather + use(xlink:href="data/icons.svg#clock") + button.inline.square(onclick="{switchSort('name')}" class="{selected: sort === 'name' && !searchResults}") + svg.feather + use(xlink:href="data/icons.svg#sort-alphabetically") + .aSearchWrap + input.inline(type="text" onkeyup="{fuseSearch}") + svg.feather + use(xlink:href="data/icons.svg#search") + button.inline.square(onclick="{switchLayout}") + svg.feather + use(xlink:href="data/icons.svg#{localStorage.roomsLayout === 'list'? 'grid' : 'list'}") + .toleft + button#roomcreate(onclick="{roomCreate}" data-hotkey="Control+n" title="Control+N") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.create} + ul.cards.rooms.flexfix-body(class="{list: localStorage.roomsLayout === 'list'}") + li( + each="{room in (searchResults? searchResults : rooms)}" + class="{starting: global.currentProject.startroom === room.uid}" + onclick="{openRoom(room)}" + oncontextmenu="{menuPopup(room)}" + onlong-press="{menuPopup(room)}" + ) + img(src="file://{global.projdir + '/img/r' + room.thumbnail + '.png?' + room.lastmod}") + span {room.name} + span.date(if="{room.lastmod}") {niceTime(room.lastmod)} + svg.feather(if="{global.currentProject.startroom === room.uid}") + use(xlink:href="data/icons.svg#play") + room-editor(if="{editing}" room="{editingRoom}") + context-menu(menu="{roomMenu}" ref="roomMenu") + script. + const generateGUID = require('./data/node_requires/generateGUID'); + + this.namespace = 'rooms'; + this.mixin(window.riotVoc); + this.mixin(window.riotNiceTime); + this.editing = false; + this.sort = 'name'; + this.sortReverse = false; + + this.updateList = () => { + this.rooms = [...global.currentProject.rooms]; + if (this.sort === 'name') { + this.rooms.sort((a, b) => a.name.localeCompare(b.name)); + } else { + this.rooms.sort((a, b) => b.lastmod - a.lastmod); + } + if (this.sortReverse) { + this.rooms.reverse(); + } + }; + this.switchSort = sort => () => { + if (this.sort === sort) { + this.sortReverse = !this.sortReverse; + } else { + this.sort = sort; + this.sortReverse = false; + } + this.updateList(); + }; + this.switchLayout = () => { + localStorage.roomsLayout = localStorage.roomsLayout === 'list' ? 'grid' : 'list'; + }; + const fuseOptions = { + shouldSort: true, + tokenize: true, + threshold: 0.5, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['name'] + }; + const Fuse = require('fuse.js'); + this.fuseSearch = e => { + if (e.target.value.trim()) { + var fuse = new Fuse(this.rooms, fuseOptions); + this.searchResults = fuse.search(e.target.value.trim()); + } else { + this.searchResults = null; + } + }; + this.setUpPanel = () => { + this.updateList(); + this.searchResults = null; + this.editing = false; + this.editingRoom = null; + this.update(); + }; + window.signals.on('projectLoaded', this.setUpPanel); + this.on('mount', this.setUpPanel); + this.on('unmount', () => { + window.signals.off('projectLoaded', this.setUpPanel); + }); + + const fs = require('fs-extra'), + path = require('path'); + this.roomCreate = function roomCreate() { + if (this.editing) { + return false; + } + var guid = generateGUID(), + thumbnail = guid.split('-').pop(); + fs.copy('./data/img/notexture.png', path.join(global.projdir, '/img/r' + thumbnail + '.png'), () => { + var newRoom = { + name: 'Room_' + thumbnail, + oncreate: '', + onstep: '', + ondraw: '', + onleave: '', + width: 800, + height: 600, + backgrounds: [], + copies: [], + tiles: [], + uid: guid, + thumbnail + }; + global.currentProject.rooms.push(newRoom); + this.openRoom(newRoom)(); + this.updateList(); + this.update(); + }); + return true; + }; + this.openRoom = room => () => { + this.editingRoom = room; + this.editing = true; + }; + + this.roomMenu = { + items: [{ + label: this.voc.makestarting, + click: () => { + global.currentProject.startroom = this.editingRoom.uid; + this.update(); + } + }, { + label: window.languageJSON.common.open, + click: () => { + this.openRoom(this.editingRoom)(); + this.update(); + } + }, { + label: window.languageJSON.common.copyName, + click: () => { + nw.Clipboard.get().set(this.editingRoom.name, 'text'); + } + }, { + label: window.languageJSON.common.duplicate, + click: () => { + window.alertify + .defaultValue(this.editingRoom.name + '_dup') + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { + var guid = generateGUID(), + thumbnail = guid.split('-').pop(); + var newRoom = JSON.parse(JSON.stringify(this.editingRoom)); + newRoom.name = e.inputValue; + global.currentProject.rooms.push(newRoom); + newRoom.uid = guid; + newRoom.thumbnail = thumbnail; + fs.linkSync(global.projdir + '/img/r' + this.editingRoom.thumbnail + '.png', global.projdir + '/img/r' + thumbnail + '.png'); + this.updateList(); + this.update(); + } + }); + } + }, { + label: window.languageJSON.common.rename, + click: () => { + window.alertify + .defaultValue(this.editingRoom.name) + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { + var nam = e.inputValue; + this.editingRoom.name = nam; + this.update(); + } + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.common.delete, + click: () => { + window.alertify + .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editingRoom.name)) + .then(e => { + if (e.buttonClicked === 'ok') { + var ind = global.currentProject.rooms.indexOf(this.editingRoom); + global.currentProject.rooms.splice(ind, 1); + this.updateList(); + this.update(); + window.alertify + .okBtn(window.languageJSON.common.ok) + .cancelBtn(window.languageJSON.common.cancel); + } + }); + } + }] + }; + + this.menuPopup = room => e => { + this.editingRoom = room; + this.refs.roomMenu.popup(e.clientX, e.clientY); + e.preventDefault(); + }; diff --git a/src/riotTags/root-tag.tag b/src/riotTags/root-tag.tag index dc22f41c1..cb1cf2611 100644 --- a/src/riotTags/root-tag.tag +++ b/src/riotTags/root-tag.tag @@ -1,27 +1,27 @@ -root-tag - main-menu(if="{!selectorVisible}") - notepad-panel(if="{!selectorVisible}") - project-selector(if="{selectorVisible}") - script. - this.selectorVisible = true; - window.signals.on('resetAll', () => { - global.currentProject = false; - this.selectorVisible = true; - riot.update(); - }); - - const stylesheet = document.createElement('style'); - document.head.appendChild(stylesheet); - const updateStylesheet = () => { - stylesheet.innerHTML = ` - code, pre { - font-family: ${localStorage.fontFamily || 'Iosevka, monospace'}; - font-variant-ligatures: ${localStorage.codeLigatures === 'off'? 'none' : 'normal'}; - } - .monaco-editor .view-lines.view-lines { - line-height: ${localStorage.codeDense === 'on'? 1.5 : 1.75}; - } - `; - }; - updateStylesheet(); +root-tag + main-menu(if="{!selectorVisible}") + notepad-panel(if="{!selectorVisible}") + project-selector(if="{selectorVisible}") + script. + this.selectorVisible = true; + window.signals.on('resetAll', () => { + global.currentProject = false; + this.selectorVisible = true; + riot.update(); + }); + + const stylesheet = document.createElement('style'); + document.head.appendChild(stylesheet); + const updateStylesheet = () => { + stylesheet.innerHTML = ` + code, pre { + font-family: ${localStorage.fontFamily || 'Iosevka, monospace'}; + font-variant-ligatures: ${localStorage.codeLigatures === 'off' ? 'none' : 'normal'}; + } + .monaco-editor .view-lines.view-lines { + line-height: ${localStorage.codeDense === 'on' ? 1.5 : 1.75}; + } + `; + }; + updateStylesheet(); window.signals.on('codeFontUpdated', updateStylesheet); \ No newline at end of file diff --git a/src/riotTags/script-editor.tag b/src/riotTags/script-editor.tag index c476ec6d1..6a634bb84 100644 --- a/src/riotTags/script-editor.tag +++ b/src/riotTags/script-editor.tag @@ -23,13 +23,13 @@ script-editor.view.panel window.removeEventListener('resize', updateEditorSize); window.signals.off('settingsFocus', updateEditorSizeDeferred); }); - this.on('mount', e => { + this.on('mount', () => { setTimeout(() => { var editorOptions = { language: 'javascript' }; this.editor = window.setupCodeEditor(this.refs.editor, editorOptions); - this.editor.onDidChangeModelContent(e => { + this.editor.onDidChangeModelContent(() => { this.script.code = this.editor.getValue(); }); this.editor.setValue(this.script.code); @@ -43,7 +43,7 @@ script-editor.view.panel this.editor.dispose(); }); - this.saveScript = e => { + this.saveScript = () => { const glob = require('./data/node_requires/glob'); if (glob.scriptTypings[this.oldName]) { for (const lib of glob.scriptTypings[this.oldName]) { @@ -59,4 +59,6 @@ script-editor.view.panel this.parent.update(); }; - this.updateScriptName = e => this.script.name = e.target.value.trim(); \ No newline at end of file + this.updateScriptName = e => { + this.script.name = e.target.value.trim(); + }; \ No newline at end of file diff --git a/src/riotTags/scripts-panel.tag b/src/riotTags/scripts-panel.tag index a0e16dcad..26618e550 100644 --- a/src/riotTags/scripts-panel.tag +++ b/src/riotTags/scripts-panel.tag @@ -24,7 +24,16 @@ scripts-panel this.currentProject = global.currentProject; this.currentProject.scripts = this.currentProject.scripts || []; - this.addNewScript = e => { + const glob = require('./data/node_requires/glob'); + glob.scriptTypings = glob.scriptTypings || {}; + for (const script of global.currentProject.scripts) { + glob.scriptTypings[script.name] = [ + monaco.languages.typescript.javascriptDefaults.addExtraLib(script.code), + monaco.languages.typescript.typescriptDefaults.addExtraLib(script.code) + ]; + } + + this.addNewScript = () => { var script = { name: 'New Script', code: `/* ${this.voc.newScriptComment} */` @@ -36,7 +45,7 @@ scripts-panel this.currentScript = e.item.script; }; this.deleteScript = e => { - const script = e.item.script, + const {script} = e.item, ind = this.currentProject.scripts.indexOf(script); this.currentProject.scripts.splice(ind, 1); for (const lib of glob.scriptTypings[script.name]) { @@ -48,51 +57,40 @@ scripts-panel this.moveUp = e => { e.stopPropagation(); - let script = e.item.script; + const clickedScript = e.item.script; + const top = []; + const bottom = []; let topPush = true; - let top = []; - let bottom = []; - for (const element of this.currentProject.scripts) { - if (element === script) { + for (const projScript of this.currentProject.scripts) { + if (projScript === clickedScript) { topPush = false; } else if (topPush) { - top.push(element); + top.push(projScript); } else { - bottom.push(element); + bottom.push(projScript); } } - top.splice(top.length - 1, 0, script); + top.splice(top.length - 1, 0, clickedScript); const out = [...top, ...bottom]; - console.debug(out); this.currentProject.scripts = out; }; this.moveDown = e => { e.stopPropagation(); - let script = e.item.script; + const clickedScript = e.item.script; + const top = []; + const bottom = []; let topPush = true; - let top = []; - let bottom = []; - for (const element of this.currentProject.scripts) { - if (element === script) { + for (const projScript of this.currentProject.scripts) { + if (projScript === clickedScript) { topPush = false; } else if (topPush) { - top.push(element); + top.push(projScript); } else { - bottom.push(element); + bottom.push(projScript); } } - bottom.splice(1, 0, script); + bottom.splice(1, 0, clickedScript); const out = [...top, ...bottom]; - console.debug(out); this.currentProject.scripts = out; - }; - - const glob = require('./data/node_requires/glob'); - glob.scriptTypings = glob.scriptTypings || {}; - for (const script of global.currentProject.scripts) { - glob.scriptTypings[script.name] = [ - monaco.languages.typescript.javascriptDefaults.addExtraLib(script.code), - monaco.languages.typescript.typescriptDefaults.addExtraLib(script.code) - ]; - } \ No newline at end of file + }; \ No newline at end of file diff --git a/src/riotTags/settings-panel.tag b/src/riotTags/settings-panel.tag index 5aabeb1bf..ad96a6452 100644 --- a/src/riotTags/settings-panel.tag +++ b/src/riotTags/settings-panel.tag @@ -1,95 +1,95 @@ -settings-panel.panel.view - .tall.fifty.npl.npt.npb - h1 {voc.settings} - fieldset - h2 {voc.authoring} - b {voc.title} - br - input#gametitle(type="text" value="{global.currentProject.settings.title}" onchange="{changeTitle}") - br - b {voc.author} - br - input#gameauthor(type="text" value="{global.currentProject.settings.author}" onchange="{wire('this.currentProject.settings.author')}") - br - b {voc.site} - br - input#gamesite(type="text" value="{global.currentProject.settings.site}" onchange="{wire('this.currentProject.settings.site')}") - br - b {voc.version} - br - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[0]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.0')}") - | . - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[1]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.1')}") - | . - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[2]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.2')}") - | {voc.versionpostfix} - input(type="text" style="width: 3rem;" value="{global.currentProject.settings.versionPostfix}" length="5" onchange="{wire('this.currentProject.settings.versionPostfix')}") - fieldset - h2 {voc.actions} - button.nml(onclick="{openActionsEditor}") - svg.feather - use(xlink:href="data/icons.svg#airplay") - span {voc.editActions} - fieldset - h2 {voc.branding.heading} - .block - b - span {voc.branding.icon} - hover-hint(text="{voc.branding.iconNotice}") - br - texture-input(val="{global.currentProject.settings.branding.icon || -1}" showempty="yep" onselected="{updateGameIcon}") - .spacer - .block - b - span {voc.branding.accent} - hover-hint(text="{voc.branding.accentNotice}") - color-input(onchange="{wire('global.currentProject.settings.branding.accent', true)}" color="{global.currentProject.settings.branding.accent}") - .spacer - .block.checkbox - input(type="checkbox" value="{global.currentProject.settings.branding.invertPreloaderScheme}" checked="{global.currentProject.settings.branding.invertPreloaderScheme}" onchange="{wire('this.currentProject.settings.branding.invertPreloaderScheme')}") - span {voc.branding.invertPreloaderScheme} - fieldset - h2 {voc.renderoptions} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.pixelatedrender}" checked="{global.currentProject.settings.pixelatedrender}" onchange="{wire('this.currentProject.settings.pixelatedrender')}") - span {voc.pixelatedrender} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.highDensity}" checked="{global.currentProject.settings.highDensity}" onchange="{wire('this.currentProject.settings.highDensity')}") - span {voc.highDensity} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.usePixiLegacy}" checked="{global.currentProject.settings.usePixiLegacy}" onchange="{wire('this.currentProject.settings.usePixiLegacy')}") - span {voc.usePixiLegacy} - label.block - span {voc.maxFPS} - | - input.short(type="number" min="1" value="{global.currentProject.settings.maxFPS || 60}" onchange="{wire('this.currentProject.settings.maxFPS')}") - fieldset - h2 {voc.exportparams} - label.block.checkbox(style="margin-right: 2.5rem;") - input(type="checkbox" value="{global.currentProject.settings.minifyhtmlcss}" checked="{global.currentProject.settings.minifyhtmlcss}" onchange="{wire('this.currentProject.settings.minifyhtmlcss')}") - span {voc.minifyhtmlcss} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.minifyjs}" checked="{global.currentProject.settings.minifyjs}" onchange="{wire('this.currentProject.settings.minifyjs')}") - span {voc.minifyjs} - - scripts-panel.tall.fifty.flexfix.npr.npt.npb - actions-editor(if="{editingActions}") - script. - this.namespace = 'settings'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.currentProject = global.currentProject; - this.currentProject.settings.fps = this.currentProject.settings.fps || 30; - - this.changeTitle = e => { - global.currentProject.settings.title = e.target.value.trim(); - if (global.currentProject.settings.title) { - document.title = global.currentProject.settings.title + ' — ct.js'; - } - }; - this.updateGameIcon = tex => { - global.currentProject.settings.branding.icon = tex.uid; - }; - this.openActionsEditor = e => { - this.editingActions = true; - }; +settings-panel.panel.view + .tall.fifty.npl.npt.npb + h1 {voc.settings} + fieldset + h2 {voc.authoring} + b {voc.title} + br + input#gametitle(type="text" value="{global.currentProject.settings.title}" onchange="{changeTitle}") + br + b {voc.author} + br + input#gameauthor(type="text" value="{global.currentProject.settings.author}" onchange="{wire('this.currentProject.settings.author')}") + br + b {voc.site} + br + input#gamesite(type="text" value="{global.currentProject.settings.site}" onchange="{wire('this.currentProject.settings.site')}") + br + b {voc.version} + br + input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[0]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.0')}") + | . + input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[1]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.1')}") + | . + input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[2]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.2')}") + | {voc.versionpostfix} + input(type="text" style="width: 3rem;" value="{global.currentProject.settings.versionPostfix}" length="5" onchange="{wire('this.currentProject.settings.versionPostfix')}") + fieldset + h2 {voc.actions} + button.nml(onclick="{openActionsEditor}") + svg.feather + use(xlink:href="data/icons.svg#airplay") + span {voc.editActions} + fieldset + h2 {voc.branding.heading} + .block + b + span {voc.branding.icon} + hover-hint(text="{voc.branding.iconNotice}") + br + texture-input(val="{global.currentProject.settings.branding.icon || -1}" showempty="yep" onselected="{updateGameIcon}") + .spacer + .block + b + span {voc.branding.accent} + hover-hint(text="{voc.branding.accentNotice}") + color-input(onchange="{wire('global.currentProject.settings.branding.accent', true)}" color="{global.currentProject.settings.branding.accent}") + .spacer + .block.checkbox + input(type="checkbox" value="{global.currentProject.settings.branding.invertPreloaderScheme}" checked="{global.currentProject.settings.branding.invertPreloaderScheme}" onchange="{wire('this.currentProject.settings.branding.invertPreloaderScheme')}") + span {voc.branding.invertPreloaderScheme} + fieldset + h2 {voc.renderoptions} + label.block.checkbox + input(type="checkbox" value="{global.currentProject.settings.pixelatedrender}" checked="{global.currentProject.settings.pixelatedrender}" onchange="{wire('this.currentProject.settings.pixelatedrender')}") + span {voc.pixelatedrender} + label.block.checkbox + input(type="checkbox" value="{global.currentProject.settings.highDensity}" checked="{global.currentProject.settings.highDensity}" onchange="{wire('this.currentProject.settings.highDensity')}") + span {voc.highDensity} + label.block.checkbox + input(type="checkbox" value="{global.currentProject.settings.usePixiLegacy}" checked="{global.currentProject.settings.usePixiLegacy}" onchange="{wire('this.currentProject.settings.usePixiLegacy')}") + span {voc.usePixiLegacy} + label.block + span {voc.maxFPS} + | + input.short(type="number" min="1" value="{global.currentProject.settings.maxFPS || 60}" onchange="{wire('this.currentProject.settings.maxFPS')}") + fieldset + h2 {voc.exportparams} + label.block.checkbox(style="margin-right: 2.5rem;") + input(type="checkbox" value="{global.currentProject.settings.minifyhtmlcss}" checked="{global.currentProject.settings.minifyhtmlcss}" onchange="{wire('this.currentProject.settings.minifyhtmlcss')}") + span {voc.minifyhtmlcss} + label.block.checkbox + input(type="checkbox" value="{global.currentProject.settings.minifyjs}" checked="{global.currentProject.settings.minifyjs}" onchange="{wire('this.currentProject.settings.minifyjs')}") + span {voc.minifyjs} + + scripts-panel.tall.fifty.flexfix.npr.npt.npb + actions-editor(if="{editingActions}") + script. + this.namespace = 'settings'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.currentProject = global.currentProject; + this.currentProject.settings.fps = this.currentProject.settings.fps || 30; + + this.changeTitle = e => { + global.currentProject.settings.title = e.target.value.trim(); + if (global.currentProject.settings.title) { + document.title = global.currentProject.settings.title + ' — ct.js'; + } + }; + this.updateGameIcon = tex => { + global.currentProject.settings.branding.icon = tex.uid; + }; + this.openActionsEditor = () => { + this.editingActions = true; + }; diff --git a/src/riotTags/shared/asset-viewer.tag b/src/riotTags/shared/asset-viewer.tag index b7efb0f8c..f5954d073 100644 --- a/src/riotTags/shared/asset-viewer.tag +++ b/src/riotTags/shared/asset-viewer.tag @@ -90,19 +90,15 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") this.updateList = () => { this.collection = [...(this.opts.collection || [])]; if (this.sort === 'name') { - this.collection.sort((a, b) => { - return a.name.localeCompare(b.name); - }); + this.collection.sort((a, b) => a.name.localeCompare(b.name)); } else { - this.collection.sort((a, b) => { - return b.lastmod - a.lastmod; - }); + this.collection.sort((a, b) => b.lastmod - a.lastmod); } if (this.sortReverse) { this.collection.reverse(); } }; - this.switchSort = sort => e => { + this.switchSort = sort => () => { if (this.sort === sort) { this.sortReverse = !this.sortReverse; } else { @@ -120,7 +116,7 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") this.searchResults = null; } }; - this.switchLayout = e => { - const key = this.opts.namespace? (this.opts.namespace+'Layout') : 'defaultAssetLayout'; - localStorage[key] = localStorage[key] === 'list'? 'grid' : 'list'; + this.switchLayout = () => { + const key = this.opts.namespace ? (this.opts.namespace + 'Layout') : 'defaultAssetLayout'; + localStorage[key] = localStorage[key] === 'list' ? 'grid' : 'list'; }; \ No newline at end of file diff --git a/src/riotTags/shared/collapsible-section.tag b/src/riotTags/shared/collapsible-section.tag index fe4b22b26..75c4b4815 100644 --- a/src/riotTags/shared/collapsible-section.tag +++ b/src/riotTags/shared/collapsible-section.tag @@ -18,7 +18,7 @@ collapsible-section .collapsible-section-aWrapper(if="{opened}") script. - this.opened = this.opts.defaultstate === 'opened'? true : false; + this.opened = this.opts.defaultstate === 'opened'; this.toggle = () => { this.opened = !this.opened; if (this.opts.ontoggle) { diff --git a/src/riotTags/shared/color-input.tag b/src/riotTags/shared/color-input.tag index 3b3fc1f08..c8ff1c1f4 100644 --- a/src/riotTags/shared/color-input.tag +++ b/src/riotTags/shared/color-input.tag @@ -20,11 +20,12 @@ color-input onapply="{applyColor}" onchanged="{changeColor}" oncancel="{cancelColor}" ) script. - const Color = net.brehaut.Color; + /* global net */ + const brehautColor = net.brehaut.Color; this.opened = false; this.value = this.lastValue = this.opts.color || '#FFFFFF'; - this.dark = Color(this.value).getLuminance() < 0.5; - this.openPicker = e => { + this.dark = brehautColor(this.value).getLuminance() < 0.5; + this.openPicker = () => { this.opened = !this.opened; }; this.changeColor = color => { @@ -58,7 +59,7 @@ color-input this.update(); }; this.on('update', () => { - if (this.lastValue != this.opts.color) { + if (this.lastValue !== this.opts.color) { this.value = this.lastValue = this.opts.color || '#FFFFFF'; } }); \ No newline at end of file diff --git a/src/riotTags/shared/color-picker.tag b/src/riotTags/shared/color-picker.tag index 7723b7589..287e158e2 100644 --- a/src/riotTags/shared/color-picker.tag +++ b/src/riotTags/shared/color-picker.tag @@ -72,14 +72,15 @@ color-picker use(xlink:href="data/icons.svg#apply") span {vocGlob.apply} script. - const Color = net.brehaut.Color; + /* global net */ + const brehautColor = net.brehaut.Color; this.namespace = 'colorPicker'; this.mixin(window.riotVoc); this.loadColor = color => { - this.color = Color(color); + this.color = brehautColor(color); this.color = this.color.setValue(this.color.getValue()); - this.oldColor = Color(color); + this.oldColor = brehautColor(color); }; this.loadColor(this.opts.color || '#ffffff'); @@ -116,29 +117,30 @@ color-picker e.stopPropagation(); }; this.tryInputColor = e => { - this.color = Color(e.target.value); + this.color = brehautColor(e.target.value); this.notifyUpdates(); e.stopPropagation(); }; this.onSwatchClick = e => { - if (e.ctrlKey) { + if (e.ctrlKey) { // deletes a swatch if (e.target.parentNode === this.refs.localSwatches) { - global.currentProject.palette.splice(global.currentProject.palette.indexOf(e.item.colr), 1); + const ind = global.currentProject.palette.indexOf(e.item.colr); + global.currentProject.palette.splice(ind, 1); } else { this.globalPalette.splice(this.globalPalette.indexOf(e.item.colr), 1); localStorage.globalPalette = JSON.stringify(this.globalPalette); } } else { - this.color = Color(e.item.colr); + this.color = brehautColor(e.item.colr); this.notifyUpdates(); } }; - this.addAsGlobal = e => { + this.addAsGlobal = () => { this.globalPalette.push(this.color.toString()); localStorage.globalPalette = JSON.stringify(this.globalPalette); }; - this.addAsLocal = e => { + this.addAsLocal = () => { global.currentProject.palette.push(this.color.toString()); }; @@ -148,13 +150,13 @@ color-picker this.opts.onchanged(this.color.toString(), 'onchanged'); } }; - this.applyColor = e => { + this.applyColor = () => { this.dark = this.color.getLuminance() < 0.5; if (this.opts.onapply) { this.opts.onapply(this.color.toString(), 'onapply'); } }; - this.cancelColor = e => { + this.cancelColor = () => { if (this.opts.oncancel) { this.opts.oncancel(this.color.toString(), 'oncancel'); } diff --git a/src/riotTags/shared/context-menu.tag b/src/riotTags/shared/context-menu.tag index eb820d73e..02dd4de81 100644 --- a/src/riotTags/shared/context-menu.tag +++ b/src/riotTags/shared/context-menu.tag @@ -53,7 +53,9 @@ context-menu(class="{opened: opts.menu.opened}" ref="root" style="{opts.menu.col if (e.item.item.submenu && e.target.closest('context-menu') === this.root) { // prevent closing if a label with a submenu was clicked *directly* e.stopPropagation(); } - if (e.item.item.click) { // first `item` is a riot's reference to all looped vars, second is var's name in markup + // first `item` is a riot's reference to all looped vars, + // second is var's name in markup + if (e.item.item.click) { e.item.item.click(); e.stopPropagation(); } @@ -109,7 +111,7 @@ context-menu(class="{opened: opts.menu.opened}" ref="root" style="{opts.menu.col } else { e.stopPropagation(); } - } + }; this.on('mount', () => { document.addEventListener('click', clickListener); }); diff --git a/src/riotTags/shared/curve-editor.tag b/src/riotTags/shared/curve-editor.tag index b111f85b5..c5ee725f7 100644 --- a/src/riotTags/shared/curve-editor.tag +++ b/src/riotTags/shared/curve-editor.tag @@ -135,10 +135,11 @@ curve-editor(ref="root") ) .clear script. + /* global net */ this.namespace = 'curveEditor'; this.mixin(window.riotVoc); this.mixin(window.riotWired); - const Color = net.brehaut.Color; + const brehautColor = net.brehaut.Color; this.uid = require('./data/node_requires/generateGUID')(); @@ -162,7 +163,7 @@ curve-editor(ref="root") this.update(); }; - this.selectedPoint = this.opts.curve[0]; + [this.selectedPoint] = this.opts.curve; this.niceNumber = number => { if (number < 10 && number > -10) { @@ -171,9 +172,9 @@ curve-editor(ref="root") return Math.round(number); }; this.updateLayout = () => { - this.selectedPoint = this.opts.curve[0]; + [this.selectedPoint] = this.opts.curve; if (this.opts.colorcurve) { - this.selectedColorPoint = this.opts.colorcurve[0]; + [this.selectedColorPoint] = this.opts.colorcurve; } const box = this.refs.root.getBoundingClientRect(); this.curve = this.opts.curve || [{ @@ -193,10 +194,6 @@ curve-editor(ref="root") }; setTimeout(this.updateLayout, 0); - const pointToLocation = point => ({ - x: point.time * this.width, - y: (1 - (point.value - this.min) / this.max) * height - }); let startMoveX, startMoveY, oldTime, oldValue; this.startMoving = point => e => { this.selectedPoint = point; @@ -259,19 +256,25 @@ curve-editor(ref="root") // Find the two points surrounding a new one for blending for (let i = 1; i < this.opts.curve.length; i++) { if (this.opts.colorcurve[i].time > point.time) { - fromPoint = this.opts.colorcurve[i-1]; - toPoint = this.opts.colorcurve[i] + fromPoint = this.opts.colorcurve[i - 1]; + toPoint = this.opts.colorcurve[i]; break; } } if (!fromPoint || !toPoint) { return; } - const color1 = Color('#' + fromPoint.value), - color2 = Color('#' + toPoint.value); + // eslint-disable-next-line new-cap + const color1 = brehautColor('#' + fromPoint.value), + // eslint-disable-next-line new-cap + color2 = brehautColor('#' + toPoint.value); + const mixedColor = (point.time - fromPoint.time) / (toPoint.time - fromPoint.time); this.opts.colorcurve.push({ time: point.time, - value: color1.blend(color2, (point.time - fromPoint.time) / (toPoint.time - fromPoint.time)).toString().slice(1) + value: color1 + .blend(color2, mixedColor) + .toString() + .slice(1) }); this.opts.colorcurve.sort((a, b) => a.time - b.time); } @@ -298,9 +301,9 @@ curve-editor(ref="root") o.colorcurve.splice(ind, 1); } if (spliced[0] === this.selectedPoint) { - this.selectedPoint = o.curve[0]; + [this.selectedPoint] = o.curve; if (this.opts.type === 'color') { - this.selectedColorPoint = o.colorcurve[0]; + [this.selectedColorPoint] = o.colorcurve; } } if (this.opts.onchange) { @@ -309,7 +312,7 @@ curve-editor(ref="root") } }; - const onMouseUp = e => { + const onMouseUp = () => { this.movedPoint = false; }; this.on('mount', () => { @@ -319,8 +322,7 @@ curve-editor(ref="root") document.removeEventListener('mouseup', onMouseUp); }); - this.getPointTop = point => { - return (1 - (point.value - this.min) / (this.max-this.min)) * this.height; - }; + this.getPointTop = point => + (1 - (point.value - this.min) / (this.max - this.min)) * this.height; this.getPointLeft = point => (point.time - this.minTime) / this.maxTime * this.width; diff --git a/src/riotTags/shared/docs-shortcut.tag b/src/riotTags/shared/docs-shortcut.tag index daf8da95f..68d9bb4fa 100644 --- a/src/riotTags/shared/docs-shortcut.tag +++ b/src/riotTags/shared/docs-shortcut.tag @@ -19,7 +19,7 @@ docs-shortcut script. this.namespace = 'docsShortcut'; this.mixin(window.riotVoc); - this.navigateToDocs = e => { + this.navigateToDocs = () => { window.signals.trigger('openDocs', { path: this.opts.path || '/' }); diff --git a/src/riotTags/shared/texture-input.tag b/src/riotTags/shared/texture-input.tag index 4d330d83b..9c12bfcdb 100644 --- a/src/riotTags/shared/texture-input.tag +++ b/src/riotTags/shared/texture-input.tag @@ -28,10 +28,10 @@ texture-input this.getTextureFromId = getTextureFromId; this.val = this.opts.val || -1; - this.openSelector = e => { + this.openSelector = () => { this.selectingTexture = true; }; - this.onSelected = texture => e => { + this.onSelected = texture => () => { if (this.opts.onselected) { this.opts.onselected(texture, texture.uid); } diff --git a/src/riotTags/shared/texture-selector.tag b/src/riotTags/shared/texture-selector.tag index 5b0e3bc11..94c20f4b5 100644 --- a/src/riotTags/shared/texture-selector.tag +++ b/src/riotTags/shared/texture-selector.tag @@ -50,19 +50,15 @@ texture-selector.panel.view this.updateList = () => { this.textures = [...global.currentProject.textures]; if (this.sort === 'name') { - this.textures.sort((a, b) => { - return a.name.localeCompare(b.name); - }); + this.textures.sort((a, b) => a.name.localeCompare(b.name)); } else { - this.textures.sort((a, b) => { - return b.lastmod - a.lastmod; - }); + this.textures.sort((a, b) => b.lastmod - a.lastmod); } if (this.sortReverse) { this.textures.reverse(); } }; - this.switchSort = sort => e => { + this.switchSort = sort => () => { if (this.sort === sort) { this.sortReverse = !this.sortReverse; } else { diff --git a/src/riotTags/sound-editor.tag b/src/riotTags/sound-editor.tag index cd959fac9..2bac3d6d1 100644 --- a/src/riotTags/sound-editor.tag +++ b/src/riotTags/sound-editor.tag @@ -1,85 +1,85 @@ -sound-editor.panel.view - .modal - b {voc.name} - br - input.wide(type="text" value="{sound.name}" onchange="{wire('this.sound.name')}") - .anErrorNotice(if="{nameTaken}") {vocGlob.nametaken} - br - p - label - b {voc.poolSize} - input(type="number" min="1" max="32" value="{sound.poolSize || 5}" onchange="{wire('this.sound.poolSize')}") - audio( - if="{sound && sound.origname}" - ref="audio" controls loop - src="file://{global.projdir + '/snd/' + sound.origname + '?' + sound.lastmod}" - onplay="{notifyPlayerPlays}" - ) - p - label.checkbox - input(type="checkbox" checked="{sound.isMusic}" onchange="{wire('this.sound.isMusic')}") - span {voc.isMusicFile} - label.file - .button.wide.nml - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.import} - input(type="file" ref="inputsound" accept=".mp3,.ogg,.wav" onchange="{changeSoundFile}") - p.nmb - button.wide(onclick="{soundSave}" title="Shift+Control+S" data-hotkey="Control+S") - svg.feather - use(xlink:href="data/icons.svg#check") - span {voc.save} - script. - const path = require('path'); - const fs = require('fs-extra'); - this.namespace = 'soundview'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.playing = false; - this.sound = this.opts.sound; - this.on('update', () => { - if (global.currentProject.sounds.find(sound => - this.sound.name === sound.name && this.sound !== sound - )) { - this.nameTaken = true; - } else { - this.nameTaken = false; - } - }) - this.notifyPlayerPlays = e => { - this.playing = true; - }; - this.soundSave = e => { - if (this.playing) { - this.togglePlay(); - } - this.parent.editing = false; - this.parent.update(); - }; - this.togglePlay = function () { - if (this.playing) { - this.playing = false; - this.refs.audio.pause(); - } else { - this.playing = true; - this.refs.audio.play(); - } - }; - this.changeSoundFile = () => { - const val = this.refs.inputsound.files[0].path; - fs.copy(val, global.projdir + '/snd/s' + this.sound.uid + path.extname(val), e => { - if (e) { - console.error(e); - alertify.error(e); - return; - } - if (!this.sound.lastmod && this.sound.name === 'Sound_' + this.sound.uid.split('-').pop()) { - this.sound.name = path.basename(val, path.extname(val)); - } - this.sound.origname = 's' + this.sound.uid + path.extname(val); - this.sound.lastmod = +(new Date()); - this.update(); - }); - this.refs.inputsound.value = ''; - }; +sound-editor.panel.view + .modal + b {voc.name} + br + input.wide(type="text" value="{sound.name}" onchange="{wire('this.sound.name')}") + .anErrorNotice(if="{nameTaken}") {vocGlob.nametaken} + br + p + label + b {voc.poolSize} + input(type="number" min="1" max="32" value="{sound.poolSize || 5}" onchange="{wire('this.sound.poolSize')}") + audio( + if="{sound && sound.origname}" + ref="audio" controls loop + src="file://{global.projdir + '/snd/' + sound.origname + '?' + sound.lastmod}" + onplay="{notifyPlayerPlays}" + ) + p + label.checkbox + input(type="checkbox" checked="{sound.isMusic}" onchange="{wire('this.sound.isMusic')}") + span {voc.isMusicFile} + label.file + .button.wide.nml + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.import} + input(type="file" ref="inputsound" accept=".mp3,.ogg,.wav" onchange="{changeSoundFile}") + p.nmb + button.wide(onclick="{soundSave}" title="Shift+Control+S" data-hotkey="Control+S") + svg.feather + use(xlink:href="data/icons.svg#check") + span {voc.save} + script. + const path = require('path'); + const fs = require('fs-extra'); + this.namespace = 'soundview'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.playing = false; + this.sound = this.opts.sound; + this.on('update', () => { + const sound = global.currentProject.sounds.find(sound => + this.sound.name === sound.name && this.sound !== sound); + if (sound) { + this.nameTaken = true; + } else { + this.nameTaken = false; + } + }); + this.notifyPlayerPlays = () => { + this.playing = true; + }; + this.soundSave = () => { + if (this.playing) { + this.togglePlay(); + } + this.parent.editing = false; + this.parent.update(); + }; + this.togglePlay = function togglePlay() { + if (this.playing) { + this.playing = false; + this.refs.audio.pause(); + } else { + this.playing = true; + this.refs.audio.play(); + } + }; + this.changeSoundFile = () => { + const val = this.refs.inputsound.files[0].path; + fs.copy(val, global.projdir + '/snd/s' + this.sound.uid + path.extname(val), e => { + if (e) { + console.error(e); + alertify.error(e); + return; + } + if (!this.sound.lastmod && this.sound.name === 'Sound_' + this.sound.uid.split('-').pop()) { + this.sound.name = path.basename(val, path.extname(val)); + } + this.sound.origname = 's' + this.sound.uid + path.extname(val); + this.sound.lastmod = Number(new Date()); + this.update(); + }); + this.refs.inputsound.value = ''; + }; diff --git a/src/riotTags/sounds-panel.tag b/src/riotTags/sounds-panel.tag index 2dc363899..ab282bba4 100644 --- a/src/riotTags/sounds-panel.tag +++ b/src/riotTags/sounds-panel.tag @@ -1,113 +1,114 @@ -sounds-panel.panel.view - asset-viewer( - collection="{global.currentProject.sounds}" - contextmenu="{popupMenu}" - namespace="sounds" - click="{openSound}" - thumbnails="{thumbnails}" - ref="sounds" - class="tall" - ) - button#soundcreate(onclick="{parent.soundNew}" title="Control+N" data-hotkey="Control+n") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.create} - sound-editor(if="{editing}" sound="{editedSound}") - context-menu(menu="{soundMenu}" ref="soundMenu") - script. - this.namespace = 'sounds'; - this.mixin(window.riotVoc); - this.mixin(window.riotNiceTime); - this.sort = 'name'; - this.sortReverse = false; - - this.thumbnails = sound => `data/img/${sound.isMusic? 'music' : 'wave'}.png`; - - this.setUpPanel = e => { - this.searchResults = null; - this.editing = false; - this.editedSound = null; - this.refs.sounds.updateList(); - this.update(); - }; - window.signals.on('projectLoaded', this.setUpPanel); - this.on('mount', this.setUpPanel); - this.on('unmount', () => { - window.signals.off('projectLoaded', this.setUpPanel); - }); - - this.soundNew = e => { - if (this.editing) { - return false; - } - const generateGUID = require('./data/node_requires/generateGUID'); - var id = generateGUID(), - slice = id.split('-').pop(); - var newSound = { - name: 'Sound_' + slice, - uid: id - }; - global.currentProject.sounds.push(newSound); - this.refs.sounds.updateList(); - this.openSound(newSound)(); - }; - this.openSound = sound => e => { - this.editedSound = sound; - this.editing = true; - this.update(); - }; - - // A context menu called by clicking on a sound card with RMB - this.soundMenu = { - items: [{ - label: window.languageJSON.common.open, - click: () => { - this.openSound(this.editedSound)(); - } - }, { - label: languageJSON.common.copyName, - click: e => { - nw.Clipboard.get().set(this.editedSound.name, 'text'); - } - }, { - label: window.languageJSON.common.rename, - click: () => { - alertify - .defaultValue(this.editedSound.name) - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue && e.buttonClicked !== 'cancel') { - this.editedSound.name = e.inputValue; - this.update(); - } - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.common.delete, - click: () => { - alertify - .okBtn(window.languageJSON.common.delete) - .cancelBtn(window.languageJSON.common.cancel) - .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editedSound.name)) - .then(e => { - if (e.buttonClicked === 'ok') { - var ind = global.currentProject.sounds.indexOf(this.editedSound); - global.currentProject.sounds.splice(ind, 1); - this.refs.sounds.updateList(); - this.update(); - alertify - .okBtn(window.languageJSON.common.ok) - .cancelBtn(window.languageJSON.common.cancel); - } - }); - } - }] - }; - - this.popupMenu = sound => e => { - this.editedSound = sound; - this.refs.soundMenu.popup(e.clientX, e.clientY); - e.preventDefault(); - }; +sounds-panel.panel.view + asset-viewer( + collection="{global.currentProject.sounds}" + contextmenu="{popupMenu}" + namespace="sounds" + click="{openSound}" + thumbnails="{thumbnails}" + ref="sounds" + class="tall" + ) + button#soundcreate(onclick="{parent.soundNew}" title="Control+N" data-hotkey="Control+n") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.create} + sound-editor(if="{editing}" sound="{editedSound}") + context-menu(menu="{soundMenu}" ref="soundMenu") + script. + this.namespace = 'sounds'; + this.mixin(window.riotVoc); + this.mixin(window.riotNiceTime); + this.sort = 'name'; + this.sortReverse = false; + + this.thumbnails = sound => `data/img/${sound.isMusic ? 'music' : 'wave'}.png`; + + this.setUpPanel = () => { + this.searchResults = null; + this.editing = false; + this.editedSound = null; + this.refs.sounds.updateList(); + this.update(); + }; + window.signals.on('projectLoaded', this.setUpPanel); + this.on('mount', this.setUpPanel); + this.on('unmount', () => { + window.signals.off('projectLoaded', this.setUpPanel); + }); + + this.soundNew = () => { + if (this.editing) { + return false; + } + const generateGUID = require('./data/node_requires/generateGUID'); + var id = generateGUID(), + slice = id.split('-').pop(); + var newSound = { + name: 'Sound_' + slice, + uid: id + }; + global.currentProject.sounds.push(newSound); + this.refs.sounds.updateList(); + this.openSound(newSound)(); + return true; + }; + this.openSound = sound => () => { + this.editedSound = sound; + this.editing = true; + this.update(); + }; + + // A context menu called by clicking on a sound card with RMB + this.soundMenu = { + items: [{ + label: window.languageJSON.common.open, + click: () => { + this.openSound(this.editedSound)(); + } + }, { + label: window.languageJSON.common.copyName, + click: () => { + nw.Clipboard.get().set(this.editedSound.name, 'text'); + } + }, { + label: window.languageJSON.common.rename, + click: () => { + alertify + .defaultValue(this.editedSound.name) + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue && e.buttonClicked !== 'cancel') { + this.editedSound.name = e.inputValue; + this.update(); + } + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.common.delete, + click: () => { + alertify + .okBtn(window.languageJSON.common.delete) + .cancelBtn(window.languageJSON.common.cancel) + .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editedSound.name)) + .then(e => { + if (e.buttonClicked === 'ok') { + var ind = global.currentProject.sounds.indexOf(this.editedSound); + global.currentProject.sounds.splice(ind, 1); + this.refs.sounds.updateList(); + this.update(); + alertify + .okBtn(window.languageJSON.common.ok) + .cancelBtn(window.languageJSON.common.cancel); + } + }); + } + }] + }; + + this.popupMenu = sound => e => { + this.editedSound = sound; + this.refs.soundMenu.popup(e.clientX, e.clientY); + e.preventDefault(); + }; diff --git a/src/riotTags/style-editor.tag b/src/riotTags/style-editor.tag index 12c69fd72..c71c0d13c 100644 --- a/src/riotTags/style-editor.tag +++ b/src/riotTags/style-editor.tag @@ -1,273 +1,273 @@ -style-editor.panel.view - #styleleft.tall.flexfix - .flexfix-header - .panel.pad - b {vocGlob.name} - br - input.wide(type="text" value="{styleobj.name}" onchange="{wire('this.styleobj.name')}") - .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} - .tabwrap.flexfix-body - ul.nav.tabs.nogrow.noshrink - li(onclick="{changeTab('stylefont')}" class="{active: tab === 'stylefont'}") {voc.font} - li(onclick="{changeTab('stylefill')}" class="{active: tab === 'stylefill'}") {voc.fill} - li(onclick="{changeTab('stylestroke')}" class="{active: tab === 'stylestroke'}") {voc.stroke} - li(onclick="{changeTab('styleshadow')}" class="{active: tab === 'styleshadow'}") {voc.shadow} - #stylefont.tabbed(show="{tab === 'stylefont'}") - #stylefontinner - fieldset - b {voc.fontfamily} - input#fontfamily.wide(type="text" value="{styleobj.font.family || 'sans-serif'}" onchange="{wire('this.styleobj.font.family')}") - .fifty.npl.npt - b {voc.fontsize} - br - input#fontsize.wide(type="number" value="{styleobj.font.size || '12'}" onchange="{wire('this.styleobj.font.size')}" oninput="{wire('this.styleobj.font.size')}" step="1") - .fifty.npr.npt - b {voc.fontweight} - br - select.wide(value="{styleobj.font.weight}" onchange="{wire('this.styleobj.font.weight')}") - each val in [100, 200, 300, 400, 500, 600, 700, 800, 900] - option(value=val)= val - .clear - label.checkbox - input(type="checkbox" checked="{styleobj.font.italic}" onchange="{wire('this.styleobj.font.italic')}") - span {voc.italic} - fieldset - b {voc.alignment} - .align.buttonselect - button#middleleft.inline.nml(onclick="{styleSetAlign('left')}" class="{active: this.styleobj.font.halign === 'left'}") - svg.feather - use(xlink:href="data/icons.svg#align-left") - button#middlecenter.inline(onclick="{styleSetAlign('center')}" class="{active: this.styleobj.font.halign === 'center'}") - svg.feather - use(xlink:href="data/icons.svg#align-center") - button#middleright.inline(onclick="{styleSetAlign('right')}" class="{active: this.styleobj.font.halign === 'right'}") - svg.feather - use(xlink:href="data/icons.svg#align-right") - label - b {voc.lineHeight} - br - input(type="number" step="1" min="0" value="{styleobj.font.lineHeight || 0}" oninput="{wire('this.styleobj.font.lineHeight')}") - fieldset - label.checkbox - input(type="checkbox" checked="{styleobj.font.wrap}" onchange="{wire('this.styleobj.font.wrap')}") - b {voc.textWrap} - label(if="{styleobj.font.wrap}").block.nmt - b {voc.textWrapWidth} - input.wide(type="number" step="8" min="1" value="{styleobj.font.wrapPosition || 100}" oninput="{wire('this.styleobj.font.wrapPosition')}") - - #stylefill.tabbed(show="{tab === 'stylefill'}") - label.checkbox - input#iftochangefill(type="checkbox" checked="{'fill' in styleobj}" onchange="{styleToggleFill}") - span {voc.active} - #stylefillinner(if="{styleobj.fill}") - fieldset - b {voc.filltype} - label.checkbox - input(type="radio" value="0" name="filltype" checked="{styleobj.fill.type == 0}" onchange="{wire('this.styleobj.fill.type')}") - span {voc.fillsolid} - label.checkbox - input(type="radio" value="1" name="filltype" checked="{styleobj.fill.type == 1}" onchange="{wire('this.styleobj.fill.type')}") - span {voc.fillgrad} - fieldset - .solidfill(if="{styleobj.fill.type == 0}") - b {voc.fillcolor} - br - color-input(onchange="{wire('this.styleobj.fill.color', true)}" color="{styleobj.fill.color}") - .gradientfill(if="{styleobj.fill.type == 1}") - .fifty.npl.npt - b {voc.fillcolor1} - color-input(onchange="{wire('this.styleobj.fill.color1', true)}" color="{styleobj.fill.color1}") - .fifty.npr.npt - b {voc.fillcolor2} - color-input(onchange="{wire('this.styleobj.fill.color2', true)}" color="{styleobj.fill.color2}") - .clear - b {voc.fillgradtype} - label.checkbox - input(type="radio" value="2" name="fillgradtype" onchange="{wire('this.styleobj.fill.gradtype')}") - span {voc.fillhorisontal} - label.checkbox - input(type="radio" value="1" name="fillgradtype" onchange="{wire('this.styleobj.fill.gradtype')}") - span {voc.fillvertical} - #stylestroke.tabbed(show="{tab === 'stylestroke'}") - label.checkbox - input#iftochangestroke(type="checkbox" checked="{'stroke' in styleobj}" onchange="{styleToggleStroke}") - span {voc.active} - #stylestrokeinner(if="{styleobj.stroke}") - fieldset - b {voc.strokecolor} - color-input(onchange="{wire('this.styleobj.stroke.color', true)}" color="{styleobj.stroke.color}") - fieldset - b {voc.strokeweight} - br - input#strokeweight(type="number" value="{styleobj.stroke.weight}" onchange="{wire('this.styleobj.stroke.weight')}" oninput="{wire('this.styleobj.stroke.weight')}") - #strokeweightslider - #styleshadow.tabbed(show="{tab === 'styleshadow'}") - label.checkbox - input#iftochangeshadow(type="checkbox" checked="{'shadow' in styleobj}" onchange="{styleToggleShadow}") - span {voc.active} - #styleshadowinner(if="{styleobj.shadow}") - fieldset - b {voc.shadowcolor} - color-input(onchange="{wire('this.styleobj.shadow.color', true)}" color="{styleobj.shadow.color}") - fieldset - b {voc.shadowshift} - br - input#shadowx.short(type="number" value="{styleobj.shadow.x}" onchange="{wire('this.styleobj.shadow.x')}" oninput="{wire('this.styleobj.shadow.x')}") - | × - input#shadowy.short(type="number" value="{styleobj.shadow.y}" onchange="{wire('this.styleobj.shadow.y')}" oninput="{wire('this.styleobj.shadow.y')}") - fieldset - b {voc.shadowblur} - br - input#shadowblur(type="number" value="{styleobj.shadow.blur}" min="0" onchange="{wire('this.styleobj.shadow.blur')}" oninput="{wire('this.styleobj.shadow.blur')}") - .flexfix-footer - button.wide.nogrow.noshrink(onclick="{styleSave}" title="Shift+Control+S" data-hotkey="Control+S") - svg.feather - use(xlink:href="data/icons.svg#check") - span {voc.apply} - #stylepreview.tall(ref="canvasSlot") - texture-selector(if="{selectingTexture}" onselected="{applyTexture}" ref="textureselector") - script. - const fs = require('fs-extra'); - - this.namespace = 'styleview'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.styleobj = this.opts.styleobj; - this.styleobj.font = this.styleobj.font || { - family: 'sans-serif', - size: 12, - weight: 400, - italic: false - }; - - this.changingAnyColor = false; - this.tab = 'stylefont'; - this.changeTab = tab => e => { - this.tab = tab; - }; - this.on('mount', e => { - const width = 800; - const height = 500; - this.pixiApp = new PIXI.Application({ - width, - height, - transparent: true - }); - this.refs.canvasSlot.appendChild(this.pixiApp.view); - - var labelShort = languageJSON.styleview.testtext, - labelMultiline = languageJSON.styleview.testtext.repeat(2) + '\n' + languageJSON.styleview.testtext.repeat(3) + '\n' + languageJSON.styleview.testtext, - labelLong = 'A quick blue cat jumps over the lazy frog. 0123456789 '.repeat(3), - labelThumbnail = 'Aa'; - this.pixiStyle = new PIXI.TextStyle(); - this.labelShort = new PIXI.Text(labelShort, this.pixiStyle); - this.labelMultiline = new PIXI.Text(labelMultiline, this.pixiStyle); - this.labelLong = new PIXI.Text(labelLong, this.pixiStyle); - this.labelThumbnail = new PIXI.Text(labelThumbnail, this.pixiStyle); - this.labels = [this.labelShort, this.labelLong, this.labelMultiline]; - for (const label of this.labels) { - label.anchor.x = 0.5; - label.anchor.y = 0.5; - this.pixiApp.stage.addChild(label); - label.x = width / 2; - } - this.labelShort.y = 60; - this.labelMultiline.y = 60 * 3; - this.labelLong.y = 60 * 6; - this.refreshStyleTexture(); - }); - this.on('update', () => { - if (global.currentProject.styles.find(style => - this.styleobj.name === style.name && this.styleobj !== style - )) { - this.nameTaken = true; - } else { - this.nameTaken = false; - } - }); - this.on('updated', e => { - this.refreshStyleTexture(); - }); - - this.selectingTexture = false; - - this.styleSetAlign = align => e => { - this.styleobj.font.halign = align; - }; - this.styleToggleFill = () => { - if (this.styleobj.fill) { - delete this.styleobj.fill; - } else { - this.styleobj.fill = { - - }; - } - }; - this.styleToggleStroke = function() { - if (this.styleobj.stroke) { - delete this.styleobj.stroke; - } else { - this.styleobj.stroke = { - color: '#000000', - weight: 1 - }; - } - }; - this.styleToggleShadow = function() { - if (this.styleobj.shadow) { - delete this.styleobj.shadow; - } else { - this.styleobj.shadow = { - color: '#000000', - x: 0, - y: 0, - blur: 0 - }; - } - }; - // Render a preview image in the editor - const {extend} = require('./data/node_requires/objectUtils'); - const {styleToTextStyle} = require('./data/node_requires/styleUtils'); - this.refreshStyleTexture = e => { - this.pixiStyle.reset(); - extend(this.pixiStyle, styleToTextStyle(this.styleobj)); - for (const label of this.labels) { - label.text = label.text; - } - this.pixiApp.render(); - }; - this.styleSave = function() { - if (this.nameTaken) { - // animate the error notice - require('./data/node_requires/jellify')(this.refs.errorNotice); - soundbox.play('Failure'); - return false; - } - this.styleobj.lastmod = +(new Date()); - this.styleGenPreview(global.projdir + '/img/' + this.styleobj.origname + '_prev@2.png', 128); - this.styleGenPreview(global.projdir + '/img/' + this.styleobj.origname + '_prev.png', 64).then(() => { - this.parent.editingStyle = false; - this.parent.update(); - }); - }; - - /** - * Generates a thumbnail for the current style - * @returns {Promise} - */ - this.styleGenPreview = function(destination, size) { - return new Promise((accept, decline) => { - var img = this.pixiApp.renderer.plugins.extract.base64(this.labelThumbnail); - - var data = img.replace(/^data:image\/\w+;base64,/, ''); - var buf = new Buffer(data, 'base64'); // TODO: replace as plain Buffer constructor is deprecated - fs.writeFile(destination, buf, function(err) { - if (err) { - console.error(err); - decline(err); - } else { - accept(destination); - } - }); - }); - }; +style-editor.panel.view + #styleleft.tall.flexfix + .flexfix-header + .panel.pad + b {vocGlob.name} + br + input.wide(type="text" value="{styleobj.name}" onchange="{wire('this.styleobj.name')}") + .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} + .tabwrap.flexfix-body + ul.nav.tabs.nogrow.noshrink + li(onclick="{changeTab('stylefont')}" class="{active: tab === 'stylefont'}") {voc.font} + li(onclick="{changeTab('stylefill')}" class="{active: tab === 'stylefill'}") {voc.fill} + li(onclick="{changeTab('stylestroke')}" class="{active: tab === 'stylestroke'}") {voc.stroke} + li(onclick="{changeTab('styleshadow')}" class="{active: tab === 'styleshadow'}") {voc.shadow} + #stylefont.tabbed(show="{tab === 'stylefont'}") + #stylefontinner + fieldset + b {voc.fontfamily} + input#fontfamily.wide(type="text" value="{styleobj.font.family || 'sans-serif'}" onchange="{wire('this.styleobj.font.family')}") + .fifty.npl.npt + b {voc.fontsize} + br + input#fontsize.wide(type="number" value="{styleobj.font.size || '12'}" onchange="{wire('this.styleobj.font.size')}" oninput="{wire('this.styleobj.font.size')}" step="1") + .fifty.npr.npt + b {voc.fontweight} + br + select.wide(value="{styleobj.font.weight}" onchange="{wire('this.styleobj.font.weight')}") + each val in [100, 200, 300, 400, 500, 600, 700, 800, 900] + option(value=val)= val + .clear + label.checkbox + input(type="checkbox" checked="{styleobj.font.italic}" onchange="{wire('this.styleobj.font.italic')}") + span {voc.italic} + fieldset + b {voc.alignment} + .align.buttonselect + button#middleleft.inline.nml(onclick="{styleSetAlign('left')}" class="{active: this.styleobj.font.halign === 'left'}") + svg.feather + use(xlink:href="data/icons.svg#align-left") + button#middlecenter.inline(onclick="{styleSetAlign('center')}" class="{active: this.styleobj.font.halign === 'center'}") + svg.feather + use(xlink:href="data/icons.svg#align-center") + button#middleright.inline(onclick="{styleSetAlign('right')}" class="{active: this.styleobj.font.halign === 'right'}") + svg.feather + use(xlink:href="data/icons.svg#align-right") + label + b {voc.lineHeight} + br + input(type="number" step="1" min="0" value="{styleobj.font.lineHeight || 0}" oninput="{wire('this.styleobj.font.lineHeight')}") + fieldset + label.checkbox + input(type="checkbox" checked="{styleobj.font.wrap}" onchange="{wire('this.styleobj.font.wrap')}") + b {voc.textWrap} + label(if="{styleobj.font.wrap}").block.nmt + b {voc.textWrapWidth} + input.wide(type="number" step="8" min="1" value="{styleobj.font.wrapPosition || 100}" oninput="{wire('this.styleobj.font.wrapPosition')}") + + #stylefill.tabbed(show="{tab === 'stylefill'}") + label.checkbox + input#iftochangefill(type="checkbox" checked="{'fill' in styleobj}" onchange="{styleToggleFill}") + span {voc.active} + #stylefillinner(if="{styleobj.fill}") + fieldset + b {voc.filltype} + label.checkbox + input(type="radio" value="0" name="filltype" checked="{styleobj.fill.type == 0}" onchange="{wire('this.styleobj.fill.type')}") + span {voc.fillsolid} + label.checkbox + input(type="radio" value="1" name="filltype" checked="{styleobj.fill.type == 1}" onchange="{wire('this.styleobj.fill.type')}") + span {voc.fillgrad} + fieldset + .solidfill(if="{styleobj.fill.type == 0}") + b {voc.fillcolor} + br + color-input(onchange="{wire('this.styleobj.fill.color', true)}" color="{styleobj.fill.color}") + .gradientfill(if="{styleobj.fill.type == 1}") + .fifty.npl.npt + b {voc.fillcolor1} + color-input(onchange="{wire('this.styleobj.fill.color1', true)}" color="{styleobj.fill.color1}") + .fifty.npr.npt + b {voc.fillcolor2} + color-input(onchange="{wire('this.styleobj.fill.color2', true)}" color="{styleobj.fill.color2}") + .clear + b {voc.fillgradtype} + label.checkbox + input(type="radio" value="2" name="fillgradtype" onchange="{wire('this.styleobj.fill.gradtype')}") + span {voc.fillhorisontal} + label.checkbox + input(type="radio" value="1" name="fillgradtype" onchange="{wire('this.styleobj.fill.gradtype')}") + span {voc.fillvertical} + #stylestroke.tabbed(show="{tab === 'stylestroke'}") + label.checkbox + input#iftochangestroke(type="checkbox" checked="{'stroke' in styleobj}" onchange="{styleToggleStroke}") + span {voc.active} + #stylestrokeinner(if="{styleobj.stroke}") + fieldset + b {voc.strokecolor} + color-input(onchange="{wire('this.styleobj.stroke.color', true)}" color="{styleobj.stroke.color}") + fieldset + b {voc.strokeweight} + br + input#strokeweight(type="number" value="{styleobj.stroke.weight}" onchange="{wire('this.styleobj.stroke.weight')}" oninput="{wire('this.styleobj.stroke.weight')}") + #strokeweightslider + #styleshadow.tabbed(show="{tab === 'styleshadow'}") + label.checkbox + input#iftochangeshadow(type="checkbox" checked="{'shadow' in styleobj}" onchange="{styleToggleShadow}") + span {voc.active} + #styleshadowinner(if="{styleobj.shadow}") + fieldset + b {voc.shadowcolor} + color-input(onchange="{wire('this.styleobj.shadow.color', true)}" color="{styleobj.shadow.color}") + fieldset + b {voc.shadowshift} + br + input#shadowx.short(type="number" value="{styleobj.shadow.x}" onchange="{wire('this.styleobj.shadow.x')}" oninput="{wire('this.styleobj.shadow.x')}") + | × + input#shadowy.short(type="number" value="{styleobj.shadow.y}" onchange="{wire('this.styleobj.shadow.y')}" oninput="{wire('this.styleobj.shadow.y')}") + fieldset + b {voc.shadowblur} + br + input#shadowblur(type="number" value="{styleobj.shadow.blur}" min="0" onchange="{wire('this.styleobj.shadow.blur')}" oninput="{wire('this.styleobj.shadow.blur')}") + .flexfix-footer + button.wide.nogrow.noshrink(onclick="{styleSave}" title="Shift+Control+S" data-hotkey="Control+S") + svg.feather + use(xlink:href="data/icons.svg#check") + span {voc.apply} + #stylepreview.tall(ref="canvasSlot") + texture-selector(if="{selectingTexture}" onselected="{applyTexture}" ref="textureselector") + script. + const fs = require('fs-extra'); + + this.namespace = 'styleview'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.styleobj = this.opts.styleobj; + this.styleobj.font = this.styleobj.font || { + family: 'sans-serif', + size: 12, + weight: 400, + italic: false + }; + + this.changingAnyColor = false; + this.tab = 'stylefont'; + this.changeTab = tab => () => { + this.tab = tab; + }; + this.on('mount', () => { + const width = 800; + const height = 500; + this.pixiApp = new PIXI.Application({ + width, + height, + transparent: true + }); + this.refs.canvasSlot.appendChild(this.pixiApp.view); + + var labelShort = window.languageJSON.styleview.testtext, + labelMultiline = window.languageJSON.styleview.testtext.repeat(2) + '\n' + window.languageJSON.styleview.testtext.repeat(3) + '\n' + window.languageJSON.styleview.testtext, + labelLong = 'A quick blue cat jumps over the lazy frog. 0123456789 '.repeat(3), + labelThumbnail = 'Aa'; + this.pixiStyle = new PIXI.TextStyle(); + this.labelShort = new PIXI.Text(labelShort, this.pixiStyle); + this.labelMultiline = new PIXI.Text(labelMultiline, this.pixiStyle); + this.labelLong = new PIXI.Text(labelLong, this.pixiStyle); + this.labelThumbnail = new PIXI.Text(labelThumbnail, this.pixiStyle); + this.labels = [this.labelShort, this.labelLong, this.labelMultiline]; + for (const label of this.labels) { + label.anchor.x = 0.5; + label.anchor.y = 0.5; + this.pixiApp.stage.addChild(label); + label.x = width / 2; + } + this.labelShort.y = 60; + this.labelMultiline.y = 60 * 3; + this.labelLong.y = 60 * 6; + this.refreshStyleTexture(); + }); + this.on('update', () => { + const {styles} = global.currentProject; + if (styles.find(style => this.styleobj.name === style.name && this.styleobj !== style)) { + this.nameTaken = true; + } else { + this.nameTaken = false; + } + }); + this.on('updated', () => { + this.refreshStyleTexture(); + }); + + this.selectingTexture = false; + + this.styleSetAlign = align => () => { + this.styleobj.font.halign = align; + }; + this.styleToggleFill = () => { + if (this.styleobj.fill) { + delete this.styleobj.fill; + } else { + this.styleobj.fill = {}; + } + }; + this.styleToggleStroke = function styleToggleStroke() { + if (this.styleobj.stroke) { + delete this.styleobj.stroke; + } else { + this.styleobj.stroke = { + color: '#000000', + weight: 1 + }; + } + }; + this.styleToggleShadow = function styleToggleShadow() { + if (this.styleobj.shadow) { + delete this.styleobj.shadow; + } else { + this.styleobj.shadow = { + color: '#000000', + x: 0, + y: 0, + blur: 0 + }; + } + }; + // Render a preview image in the editor + const {extend} = require('./data/node_requires/objectUtils'); + const {styleToTextStyle} = require('./data/node_requires/styleUtils'); + this.refreshStyleTexture = () => { + this.pixiStyle.reset(); + extend(this.pixiStyle, styleToTextStyle(this.styleobj)); + for (const label of this.labels) { + // this forces to redraw the pixi label + // eslint-disable-next-line no-self-assign + label.text = label.text; + } + this.pixiApp.render(); + }; + this.styleSave = function styleSave() { + if (this.nameTaken) { + // animate the error notice + require('./data/node_requires/jellify')(this.refs.errorNotice); + soundbox.play('Failure'); + return false; + } + this.styleobj.lastmod = Number(new Date()); + this.styleGenPreview(global.projdir + '/img/' + this.styleobj.origname + '_prev@2.png', 128); + this.styleGenPreview(global.projdir + '/img/' + this.styleobj.origname + '_prev.png', 64).then(() => { + this.parent.editingStyle = false; + this.parent.update(); + }); + return true; + }; + + /** + * Generates a thumbnail for the current style + * @returns {Promise} + */ + this.styleGenPreview = function styleGenPreview(destination) { + return new Promise((accept, decline) => { + var img = this.pixiApp.renderer.plugins.extract.base64(this.labelThumbnail); + + var thumbnailBase64 = img.replace(/^data:image\/\w+;base64,/, ''); + var buf = Buffer.from(thumbnailBase64, 'base64'); + fs.writeFile(destination, buf, err => { + if (err) { + console.error(err); + decline(err); + } else { + accept(destination); + } + }); + }); + }; diff --git a/src/riotTags/styles-panel.tag b/src/riotTags/styles-panel.tag index 1c3077602..427e5ba83 100644 --- a/src/riotTags/styles-panel.tag +++ b/src/riotTags/styles-panel.tag @@ -1,141 +1,141 @@ -styles-panel.tall.fifty - asset-viewer( - collection="{global.currentProject.styles}" - contextmenu="{onStyleContextMenu}" - namespace="styles" - click="{openStyle}" - thumbnails="{thumbnails}" - ref="styles" - class="tall" - ) - h1.nmt {voc.styles} - button#stylecreate(onclick="{parent.styleCreate}" title="Control+N" data-hotkey="Control+n") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.create} - style-editor(if="{editingStyle}" styleobj="{editedStyle}") - context-menu(menu="{styleMenu}" ref="styleMenu") - script. - const generateGUID = require('./data/node_requires/generateGUID'); - - this.editingStyle = false; - - this.namespace = 'styles'; - this.mixin(window.riotVoc); - - this.thumbnails = style => `file://${window.global.projdir}/img/${style.origname}_prev.png?${style.lastmod}`; - - this.styleCreate = e => { - if (this.editingStyle) { - return; - } - let id = generateGUID(), - slice = id.split('-').pop(); - global.currentProject.styletick ++; - let obj = { - name: "Style_" + slice, - uid: id, - origname: 's' + slice - }; - global.currentProject.styles.push(obj); - this.editedStyle = obj; - this.editingStyle = true; - this.refs.styles.updateList(); - - if (!e) { - this.update(); - } - }; - this.openStyle = style => e => { - this.editingStyle = true; - this.editedStyle = style; - }; - this.setUpPanel = e => { - this.refs.styles.updateList(); - this.editingStyle = false; - this.editedStyle = null; - this.update(); - }; - window.signals.on('projectLoaded', this.setUpPanel); - this.on('mount', this.setUpPanel); - this.on('unmount', () => { - window.signals.off('projectLoaded', this.setUpPanel); - }); - - this.onStyleContextMenu = style => e => { - this.editedStyle = style; - this.refs.styleMenu.popup(e.clientX, e.clientY); - e.preventDefault(); - }; - - this.styleMenu = { - items: [{ - label: window.languageJSON.common.open, - click: e => { - this.editingStyle = true; - this.update(); - } - }, { - label: languageJSON.common.copyName, - click: e => { - nw.Clipboard.get().set(this.editedStyle.name, 'text'); - } - }, { - label: window.languageJSON.common.duplicate, - click: () => { - alertify - .defaultValue(this.editedStyle.name + '_dup') - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { - var id = generateGUID(), - slice = id.split('-').pop(); - var newStyle = JSON.parse(JSON.stringify(this.editedStyle)); - newStyle.name = e.inputValue; - newStyle.origname = 's' + slice; - newStyle.uid = id; - global.currentProject.styles.push(newStyle); - this.editedStyleId = id; - this.editedStyle = newStyle; - this.editingStyle = true; - this.refs.styles.updateList(); - this.update(); - } - }); - } - }, { - label: window.languageJSON.common.rename, - click: () => { - alertify - .defaultValue(this.editedStyle.name) - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { - this.editedStyle.name = e.inputValue; - this.update(); - } - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.common.delete, - click: () => { - alertify - .okBtn(window.languageJSON.common.delete) - .cancelBtn(window.languageJSON.common.cancel) - .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editedStyle.name)) - .then(e => { - if (e.buttonClicked === 'ok') { - const ind = global.currentProject.styles.indexOf(this.editedStyle); - global.currentProject.styles.splice(ind, 1); - this.refs.styles.updateList(); - this.update(); - alertify - .okBtn(window.languageJSON.common.ok) - .cancelBtn(window.languageJSON.common.cancel); - } - }); - } - }] - }; +styles-panel.tall.fifty + asset-viewer( + collection="{global.currentProject.styles}" + contextmenu="{onStyleContextMenu}" + namespace="styles" + click="{openStyle}" + thumbnails="{thumbnails}" + ref="styles" + class="tall" + ) + h1.nmt {voc.styles} + button#stylecreate(onclick="{parent.styleCreate}" title="Control+N" data-hotkey="Control+n") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.create} + style-editor(if="{editingStyle}" styleobj="{editedStyle}") + context-menu(menu="{styleMenu}" ref="styleMenu") + script. + const generateGUID = require('./data/node_requires/generateGUID'); + + this.editingStyle = false; + + this.namespace = 'styles'; + this.mixin(window.riotVoc); + + this.thumbnails = style => `file://${window.global.projdir}/img/${style.origname}_prev.png?${style.lastmod}`; + + this.styleCreate = e => { + if (this.editingStyle) { + return; + } + const id = generateGUID(), + slice = id.split('-').pop(); + global.currentProject.styletick++; + const obj = { + name: 'Style_' + slice, + uid: id, + origname: 's' + slice + }; + global.currentProject.styles.push(obj); + this.editedStyle = obj; + this.editingStyle = true; + this.refs.styles.updateList(); + + if (!e) { + this.update(); + } + }; + this.openStyle = style => () => { + this.editingStyle = true; + this.editedStyle = style; + }; + this.setUpPanel = () => { + this.refs.styles.updateList(); + this.editingStyle = false; + this.editedStyle = null; + this.update(); + }; + window.signals.on('projectLoaded', this.setUpPanel); + this.on('mount', this.setUpPanel); + this.on('unmount', () => { + window.signals.off('projectLoaded', this.setUpPanel); + }); + + this.onStyleContextMenu = style => e => { + this.editedStyle = style; + this.refs.styleMenu.popup(e.clientX, e.clientY); + e.preventDefault(); + }; + + this.styleMenu = { + items: [{ + label: window.languageJSON.common.open, + click: () => { + this.editingStyle = true; + this.update(); + } + }, { + label: window.languageJSON.common.copyName, + click: () => { + nw.Clipboard.get().set(this.editedStyle.name, 'text'); + } + }, { + label: window.languageJSON.common.duplicate, + click: () => { + alertify + .defaultValue(this.editedStyle.name + '_dup') + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { + var id = generateGUID(), + slice = id.split('-').pop(); + var newStyle = JSON.parse(JSON.stringify(this.editedStyle)); + newStyle.name = e.inputValue; + newStyle.origname = 's' + slice; + newStyle.uid = id; + global.currentProject.styles.push(newStyle); + this.editedStyleId = id; + this.editedStyle = newStyle; + this.editingStyle = true; + this.refs.styles.updateList(); + this.update(); + } + }); + } + }, { + label: window.languageJSON.common.rename, + click: () => { + alertify + .defaultValue(this.editedStyle.name) + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { + this.editedStyle.name = e.inputValue; + this.update(); + } + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.common.delete, + click: () => { + alertify + .okBtn(window.languageJSON.common.delete) + .cancelBtn(window.languageJSON.common.cancel) + .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.editedStyle.name)) + .then(e => { + if (e.buttonClicked === 'ok') { + const ind = global.currentProject.styles.indexOf(this.editedStyle); + global.currentProject.styles.splice(ind, 1); + this.refs.styles.updateList(); + this.update(); + alertify + .okBtn(window.languageJSON.common.ok) + .cancelBtn(window.languageJSON.common.cancel); + } + }); + } + }] + }; diff --git a/src/riotTags/texture-editor.tag b/src/riotTags/texture-editor.tag index 4fafb660e..159253451 100644 --- a/src/riotTags/texture-editor.tag +++ b/src/riotTags/texture-editor.tag @@ -1,855 +1,898 @@ -texture-editor.panel.view - .flexrow.tall - .column.borderright.tall.column1.flexfix.nogrow.noshrink - .flexfix-body - fieldset - b {voc.name} - br - input.wide(type="text" value="{opts.texture.name}" onchange="{wire('this.texture.name')}") - .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} - label.checkbox - input#texturetiled(type="checkbox" checked="{opts.texture.tiled}" onchange="{wire('this.texture.tiled')}") - span {voc.tiled} - fieldset - b {voc.center} - .flexrow - input.short(type="number" value="{opts.texture.axis[0]}" onchange="{wire('this.texture.axis.0')}" oninput="{wire('this.texture.axis.0')}") - span.center × - input.short(type="number" value="{opts.texture.axis[1]}" onchange="{wire('this.texture.axis.1')}" oninput="{wire('this.texture.axis.1')}") - .flexrow - button.wide.nml(onclick="{textureCenter}") - span {voc.setcenter} - .spacer - button.square.nmr(onclick="{textureIsometrify}" title="{voc.isometrify}") - svg.feather - use(xlink:href="data/icons.svg#map-pin") - fieldset - b {voc.form} - label.checkbox - input(type="radio" name="collisionform" checked="{opts.texture.shape === 'circle'}" onclick="{textureSelectCircle}") - span {voc.round} - label.checkbox - input(type="radio" name="collisionform" checked="{opts.texture.shape === 'rect'}" onclick="{textureSelectRect}") - span {voc.rectangle} - label.checkbox - input(type="radio" name="collisionform" checked="{opts.texture.shape === 'strip'}" onclick="{textureSelectStrip}") - span {voc.strip} - fieldset(if="{opts.texture.shape === 'circle'}") - b {voc.radius} - br - input.wide(type="number" value="{opts.texture.r}" onchange="{wire('this.texture.r')}" oninput="{wire('this.texture.r')}") - fieldset(if="{opts.texture.shape === 'rect'}") - .center - input.short(type="number" value="{opts.texture.top}" onchange="{wire('this.texture.top')}" oninput="{wire('this.texture.top')}") - br - input.short(type="number" value="{opts.texture.left}" onchange="{wire('this.texture.left')}" oninput="{wire('this.texture.left')}") - span × - input.short(type="number" value="{opts.texture.right}" onchange="{wire('this.texture.right')}" oninput="{wire('this.texture.right')}") - br - input.short(type="number" value="{opts.texture.bottom}" onchange="{wire('this.texture.bottom')}" oninput="{wire('this.texture.bottom')}") - button.wide(onclick="{textureFillRect}") - svg.feather - use(xlink:href="data/icons.svg#maximize") - span {voc.fill} - fieldset(if="{opts.texture.shape === 'strip'}") - .flexrow.aStripPointRow(each="{point, ind in getMovableStripPoints()}") - input.short(type="number" value="{point.x}" oninput="{wire('this.texture.stripPoints.'+ ind + '.x')}") - span × - input.short(type="number" value="{point.y}" oninput="{wire('this.texture.stripPoints.'+ ind + '.y')}") - button.square.inline(title="{voc.removePoint}" onclick="{removeStripPoint}") - svg.feather - use(xlink:href="data/icons.svg#minus") - label.checkbox - input(type="checkbox" checked="{opts.texture.closedStrip}" onchange="{onClosedStripChange}" ) - span {voc.closeShape} - label.checkbox - input(type="checkbox" checked="{opts.texture.symmetryStrip}" onchange="{onSymmetryChange}") - span {voc.symmetryTool} - button.wide(onclick="{addStripPoint}") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.addPoint} - fieldset - label.checkbox - input(checked="{prevShowMask}" onchange="{wire('this.prevShowMask')}" type="checkbox") - span {voc.showmask} - .flexfix-footer - button.wide(onclick="{textureSave}" title="Shift+Control+S" data-hotkey="Control+S") - svg.feather - use(xlink:href="data/icons.svg#save") - span {window.languageJSON.common.save} - .texture-editor-anAtlas.tall( - if="{opts.texture}" - style="background-color: {previewColor};" - onmousewheel="{onMouseWheel}" - ) - .texture-editor-aCanvasWrap - canvas.texture-editor-aCanvas(ref="textureCanvas" style="transform: scale({zoomFactor}); image-rendering: {zoomFactor > 1? 'pixelated' : '-webkit-optimize-contrast'}; transform-origin: 0% 0%;") - // This div is needed to cause elements' reflow so the scrollbars update on canvas' size change - div(style="width: {zoomFactor}px; height: {zoomFactor}px;") - .aClicker( - if="{prevShowMask && opts.texture.shape === 'strip'}" - each="{seg, ind in getStripSegments()}" - style="left: {seg.left}px; top: {seg.top}px; width: {seg.width}px; transform: translate(0, -50%) rotate({seg.angle}deg);" - title="{voc.addPoint}" - onclick="{addStripPointOnSegment}" - ) - .aDragger( - if="{prevShowMask}" - style="left: {opts.texture.axis[0] * zoomFactor}px; top: {opts.texture.axis[1] * zoomFactor}px; border-radius: 0;" - title="{voc.moveCenter}" - onmousedown="{startMoving('axis')}" - ) - .aDragger( - if="{prevShowMask && opts.texture.shape === 'strip'}" - each="{point, ind in getMovableStripPoints()}" - style="left: {(point.x + texture.axis[0]) * zoomFactor}px; top: {(point.y + texture.axis[1]) * zoomFactor}px;" - title="{voc.movePoint}" - onmousedown="{startMoving('point')}" - ) - .textureview-tools - .toright - label.file(title="{voc.replacetexture}") - input(type="file" ref="textureReplacer" accept=".png,.jpg,.jpeg,.bmp,.gif" onchange="{textureReplace}") - .button.inline - svg.feather - use(xlink:href="data/icons.svg#folder") - span {voc.replacetexture} - .button.inline(title="{voc.reimport}" if="{opts.texture.source}" onclick="{reimport}") - svg.feather - use(xlink:href="data/icons.svg#refresh-ccw") - .textureview-zoom - div.button-stack.inlineblock - button#texturezoom25.inline(onclick="{textureToggleZoom(0.25)}" class="{active: zoomFactor === 0.25}") 25% - button#texturezoom50.inline(onclick="{textureToggleZoom(0.5)}" class="{active: zoomFactor === 0.5}") 50% - button#texturezoom100.inline(onclick="{textureToggleZoom(1)}" class="{active: zoomFactor === 1}") 100% - button#texturezoom200.inline(onclick="{textureToggleZoom(2)}" class="{active: zoomFactor === 2}") 200% - button#texturezoom400.inline(onclick="{textureToggleZoom(4)}" class="{active: zoomFactor === 4}") 400% - .column.column2.borderleft.tall.flexfix.nogrow.noshrink(show="{!opts.texture.tiled}") - .flexfix-body - fieldset - .flexrow - div - b {voc.cols} - br - input.wide(type="number" value="{opts.texture.grid[0]}" onchange="{wire('this.texture.grid.0')}" oninput="{wire('this.texture.grid.0')}") - span   - div - b {voc.rows} - br - input.wide(type="number" value="{opts.texture.grid[1]}" onchange="{wire('this.texture.grid.1')}" oninput="{wire('this.texture.grid.1')}") - .flexrow - div - b {voc.width} - br - input.wide(type="number" value="{opts.texture.width}" onchange="{wire('this.texture.width')}" oninput="{wire('this.texture.width')}") - span   - div - b {voc.height} - br - input.wide(type="number" value="{opts.texture.height}" onchange="{wire('this.texture.height')}" oninput="{wire('this.texture.height')}") - .flexrow - div - b {voc.marginx} - br - input.wide(type="number" value="{opts.texture.marginx}" onchange="{wire('this.texture.marginx')}" oninput="{wire('this.texture.marginx')}") - span   - div - b {voc.marginy} - br - input.wide(type="number" value="{opts.texture.marginy}" onchange="{wire('this.texture.marginy')}" oninput="{wire('this.texture.marginy')}") - .flexrow - div - b {voc.offx} - br - input.wide(type="number" value="{opts.texture.offx}" onchange="{wire('this.texture.offx')}" oninput="{wire('this.texture.offx')}") - span   - div - b {voc.offy} - br - input.wide(type="number" value="{opts.texture.offy}" onchange="{wire('this.texture.offy')}" oninput="{wire('this.texture.offy')}") - fieldset - b {voc.frames} - br - input#textureframes.wide(type="number" value="{opts.texture.untill}" onchange="{wire('this.texture.untill')}" oninput="{wire('this.texture.untill')}") - fieldset - b - span {voc.padding} - hover-hint(text="{voc.paddingNotice}") - br - input.wide(type="number" min="0" max="128" step="1" value="{opts.texture.padding}" onchange="{wire('this.texture.padding')}") - .preview.bordertop.flexfix-footer - #preview(ref="preview" style="background-color: {previewColor};") - canvas(ref="grprCanvas") - .flexrow - button#textureplay.square.inline(onclick="{currentTexturePreviewPlay}") - svg.feather - use(xlink:href="data/icons.svg#{prevPlaying? 'pause' : 'play'}") - span(ref="textureviewframe") 0 / 1 - .filler - button#textureviewback.square.inline(onclick="{currentTexturePreviewBack}") - svg.feather - use(xlink:href="data/icons.svg#skip-back") - button#textureviewnext.square.inline.nmr(onclick="{currentTexturePreviewNext}") - svg.feather - use(xlink:href="data/icons.svg#skip-forward") - .flexrow - b {voc.speed} - .filler - input#grahpspeed.short(type="number" min="1" value="{prevSpeed}" onchange="{wire('this.prevSpeed')}" oninput="{wire('this.prevSpeed')}") - .relative - button#texturecolor.inline.wide(onclick="{changeTexturePreviewColor}") - svg.feather - use(xlink:href="data/icons.svg#droplet") - span {voc.bgcolor} - input.color.rgb#previewbgcolor - - color-picker( - ref="previewBackgroundColor" if="{changingTexturePreviewColor}" - hidealpha="true" - color="{previewColor}" onapply="{updatePreviewColor}" onchanged="{updatePreviewColor}" oncancel="{cancelPreviewColor}" - ) - script. - const path = require('path'), - fs = require('fs-extra'); - const glob = require('./data/node_requires/glob'); - this.namespace = 'textureview'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - - this.nameTaken = false; - this.prevPlaying = true; - this.prevPos = 0; - this.prevSpeed = 10; - this.prevShowMask = true; - this.previewColor = localStorage.UItheme === 'Day'? '#ffffff' : '#08080D'; - this.zoomFactor = 1; - - var textureCanvas, grprCanvas; - - this.on('mount', () => { - textureCanvas = this.refs.textureCanvas; - grprCanvas = this.refs.grprCanvas; - textureCanvas.x = textureCanvas.getContext('2d'); - grprCanvas.x = grprCanvas.getContext('2d'); - var texture = this.texture = this.opts.texture; - var img = document.createElement('img'); - img.onload = () => { - textureCanvas.img = img; - this.update(); - setTimeout(() => { - this.launchTexturePreview(); - }, 0); - }; - img.onerror = e => { - alertify.error(languageJSON.textureview.corrupted); - console.error(e); - this.textureSave(); - }; - img.src = path.join('file://', global.projdir, '/img/', texture.origname) + '?' + Math.random(); - }); - this.on('update', () => { - if (global.currentProject.textures.find(texture => - this.texture.name === texture.name && this.texture !== texture - )) { - this.nameTaken = true; - } else { - this.nameTaken = false; - } - this.updateSymmetricalPoints(); - }); - this.on('updated', () => { - this.refreshTextureCanvas(); - }); - this.on('unmount', () => { - if (this.prevPlaying) { // вырубаем анимацию превью, если редактор был закрыт - this.stopTexturePreview(); - } - }); - - this.textureReplace = e => { - const val = this.refs.textureReplacer.files[0].path; - if (/\.(jpg|gif|png|jpeg)/gi.test(val)) { - this.loadImg( - this.texture.uid, - val, - global.projdir + '/img/i' + this.texture.uid + path.extname(val) - ); - this.texture.source = val; - } else { - alertify.error(window.languageJSON.common.wrongFormat); - console.log(val, 'NOT passed'); - } - this.refs.textureReplacer.value = ''; - }; - this.reimport = e => { - this.loadImg( - this.texture.uid, - this.texture.source, - global.projdir + '/img/i' + this.texture.uid + path.extname(this.texture.source) - ); - } - - /** - * Загружает изображение в редактор и генерирует квадратную превьюху из исходного изображения - * @param {Number} uid Идентификатор изображения - * @param {String} filename Путь к исходному изображению - * @param {Sting} dest Путь к изображению в папке проекта - */ - this.loadImg = (uid, filename, dest) => { - fs.copy(filename, dest, e => { - if (e) throw e; - image = document.createElement('img'); - image.onload = () => { - this.texture.imgWidth = image.width; - this.texture.imgHeight = image.height; - if (this.texture.tiled || ( - this.texture.grid[0] === 1 && - this.texture.grid[1] === 1 && - this.texture.offx === 0 && - this.texture.offy === 0 - )) { - this.texture.width = this.texture.imgWidth; - this.texture.height = this.texture.imgHeight; - } - this.texture.origname = path.basename(dest); - textureCanvas.img = image; - this.texture.lastmod = +(new Date()); - - const {imgGenPreview} = require('./data/node_requires/resources/textures'); - imgGenPreview(dest, dest + '_prev.png', 64, () => { - this.update(); - }); - imgGenPreview(dest, dest + '_prev@2.png', 128, () => {}); - setTimeout(() => { - this.refreshTextureCanvas(); - this.parent.fillTextureMap(); - this.launchTexturePreview(); - }, 0); - }; - image.onerror = e => { - alertify.error(e); - }; - image.src = 'file://' + dest + '?' + Math.random(); - }); - }; - - this.textureToggleZoom = zoom => e => { - this.zoomFactor = zoom; - }; - /** Change zoomFactor on mouse wheel roll */ - this.onMouseWheel = e => { - if (e.wheelDelta > 0) { - // in - if (this.zoomFactor === 2) { - this.zoomFactor = 4; - } else if (this.zoomFactor === 1) { - this.zoomFactor = 2; - } else if (this.zoomFactor === 0.5) { - this.zoomFactor = 1; - } else if (this.zoomFactor === 0.25) { - this.zoomFactor = 0.5; - } - } else { - // out - if (this.zoomFactor === 4) { - this.zoomFactor = 2; - } else if (this.zoomFactor === 2) { - this.zoomFactor = 1; - } else if (this.zoomFactor === 1) { - this.zoomFactor = 0.5; - } else if (this.zoomFactor === 0.5) { - this.zoomFactor = 0.25; - } - } - e.preventDefault(); - this.update(); - }; - - /** - * Установить ось вращения на центр изображения - */ - this.textureCenter = e => { - var texture = this.texture; - texture.axis[0] = Math.floor(texture.width / 2); - texture.axis[1] = Math.floor(texture.height / 2); - }; - /** - * Заполнить всё изображение маской-квадратом - */ - this.textureFillRect = e => { - var texture = this.texture; - texture.left = ~~(texture.axis[0]); - texture.top = ~~(texture.axis[1]); - texture.right = ~~(texture.width - texture.axis[0]); - texture.bottom = ~~(texture.height - texture.axis[1]); - }; - this.textureIsometrify = e => { - var texture = this.texture; - texture.axis[0] = Math.floor(texture.width / 2); - texture.axis[1] = texture.height; - this.textureFillRect(); - }; - /** - * Запустить предпросмотр анимации - */ - this.currentTexturePreviewPlay = e => { - if (this.prevPlaying) { - this.stopTexturePreview(); - } else { - this.launchTexturePreview(); - } - this.prevPlaying = !this.prevPlaying; - }; - /** - * Отступить на шаг назад в предпросмотре анимации - */ - this.currentTexturePreviewBack = e => { - this.prevPos--; - var texture = this.texture; - var total = texture.untill === 0? texture.grid[0] * texture.grid[1] : Math.min(texture.grid[0] * texture.grid[1], texture.untill); - if (this.prevPos < 0) { - this.prevPos = texture.untill === 0 ? texture.grid[0] * texture.grid[1] : total - 0; - } - this.refreshPreviewCanvas(); - }; - /** - * Шагнуть на кадр вперёд в предпросмотре анимации - */ - this.currentTexturePreviewNext = e => { - this.prevPos++; - var texture = this.texture; - var total = texture.untill === 0? texture.grid[0] * texture.grid[1] : Math.min(texture.grid[0] * texture.grid[1], texture.untill); - if (this.prevPos >= total) { - this.prevPos = 0; - } - this.refreshPreviewCanvas(); - }; - this.refreshPreviewCanvas = () => { - let xx = this.prevPos % this.texture.grid[0], - yy = Math.floor(this.prevPos / this.texture.grid[0]), - x = this.texture.offx + xx * (this.texture.marginx + this.texture.width), - y = this.texture.offy + yy * (this.texture.marginy + this.texture.height), - w = this.texture.width, - h = this.texture.height; - grprCanvas.width = w; - grprCanvas.height = h; - - grprCanvas.x.clearRect(0, 0, grprCanvas.width, grprCanvas.height); - grprCanvas.x.drawImage( - textureCanvas.img, - x, y, w, h, - 0, 0, w, h - ); - // shape - if (this.prevShowMask) { - grprCanvas.x.globalAlpha = 0.5; - grprCanvas.x.fillStyle = '#ff0'; - if (this.texture.shape == 'rect') { - grprCanvas.x.fillRect( - this.texture.axis[0] - this.texture.left, - this.texture.axis[1] - this.texture.top, - this.texture.right + this.texture.left, - this.texture.bottom + this.texture.top - ); - } else if (this.texture.shape === 'circle') { - grprCanvas.x.beginPath(); - grprCanvas.x.arc(this.texture.axis[0], this.texture.axis[1], this.texture.r, 0, 2 * Math.PI); - grprCanvas.x.fill(); - } else if (this.texture.shape === 'strip' && this.texture.stripPoints.length) { - grprCanvas.x.strokeStyle = '#ff0'; - grprCanvas.x.lineWidth = 3; - grprCanvas.x.beginPath(); - grprCanvas.x.moveTo(this.texture.stripPoints[0].x + this.texture.axis[0], this.texture.stripPoints[0].y + this.texture.axis[1]); - for (let i = 1, l = this.texture.stripPoints.length; i < l; i++) { - grprCanvas.x.lineTo(this.texture.stripPoints[i].x + this.texture.axis[0], this.texture.stripPoints[i].y + this.texture.axis[1]); - } - if (this.texture.closedStrip) { - grprCanvas.x.closePath(); - } - grprCanvas.x.stroke(); - } - grprCanvas.x.globalAlpha = 1; - grprCanvas.x.fillStyle = '#f33'; - grprCanvas.x.beginPath(); - grprCanvas.x.arc(this.texture.axis[0], this.texture.axis[1], 3, 0, 2 * Math.PI); - grprCanvas.x.fill(); - } - }; - /** - * Stops the animated preview - */ - this.stopTexturePreview = () => { - window.clearTimeout(this.prevTime); - }; - /** - * Starts the preview of a framed animation - */ - this.launchTexturePreview = () => { - var texture = this.texture; - if (this.prevTime) { - window.clearTimeout(this.prevTime); - } - this.prevPos = 0; - this.stepTexturePreview(); - }; - /** - * Шаг анимации в предпросмотре. Выполняет рендер. Записывает таймер следующего шага в this.prevTime - */ - this.stepTexturePreview = () => { - var texture = this.texture; - this.prevTime = window.setTimeout(() => { - var total = Math.min(texture.untill === 0 ? Infinity : texture.untill, texture.grid[0] * texture.grid[1]); - this.prevPos++; - if (this.prevPos >= total) { - this.prevPos = 0; - } - this.refs.textureviewframe.innerHTML = `${this.prevPos} / ${total}`; - if (!this.texture.tiled) { - this.refreshPreviewCanvas(); - } - this.stepTexturePreview(); - }, ~~(1000 / this.prevSpeed)); - }; - - /** - * Переключает тип маски на круг и выставляет начальные параметры - */ - this.textureSelectCircle = function() { - this.texture.shape = 'circle'; - if (!('r' in this.texture) || this.texture.r === 0) { - this.texture.r = Math.min( - Math.floor(this.texture.width / 2), - Math.floor(this.texture.height / 2) - ); - } - }; - /** - * Переключает тип маски на прямоугольник и выставляет начальные параметры - */ - this.textureSelectRect = function() { - this.texture.shape = 'rect'; - this.textureFillRect(); - }; - /** - * Переключает тип маски на ломаную/многоугольник и выставляет начальные параметры - */ - this.textureSelectStrip = function () { - this.texture.shape = 'strip'; - this.texture.stripPoints = this.texture.stripPoints || []; - if (!this.texture.stripPoints.length) { - const twoPi = Math.PI * 2; - this.texture.closedStrip = true; - for (let i = 0; i < 5; i++) { - this.texture.stripPoints.push({ - x: Math.round(Math.sin(twoPi / 5 * i) * this.texture.width / 2), - y: -Math.round(Math.cos(twoPi / 5 * i) * this.texture.height / 2) - }); - } - } - }; - this.removeStripPoint = function (e) { - if(this.texture.symmetryStrip) { - // Remove an extra point - this.texture.stripPoints.pop(); - } - this.texture.stripPoints.splice(e.item.ind, 1); - }; - this.addStripPoint = function () { - this.texture.stripPoints.push({ - x: 0, - y: 16 - }); - }; - this.addStripPointOnSegment = e => { - const { top, left } = textureCanvas.getBoundingClientRect(); - this.texture.stripPoints.splice(e.item.ind+1, 0, { - x: (e.pageX - left) / this.zoomFactor - this.texture.axis[0], - y: (e.pageY - top) / this.zoomFactor - this.texture.axis[1] - }); - if(this.texture.symmetryStrip) { - // Add an extra point (the symetrical point) - this.addStripPoint(); - } - }; - this.pointToLine = (linePoint1, linePoint2, point) => { - const dlx = linePoint2.x - linePoint1.x; - const dly = linePoint2.y - linePoint1.y; - const lineLength = Math.sqrt(dlx*dlx + dly*dly); - const lineAngle = Math.atan2(dly, dlx); - - const dpx = point.x - linePoint1.x; - const dpy = point.y - linePoint1.y; - const toPointLength = Math.sqrt(dpx*dpx + dpy*dpy); - const toPointAngle = Math.atan2(dpy, dpx); - - const distance = toPointLength * Math.cos(toPointAngle - lineAngle); - - return { - x: linePoint1.x + distance * dlx / lineLength, - y: linePoint1.y + distance * dly / lineLength - }; - }; - this.onClosedStripChange = e => { - this.texture.closedStrip = !this.texture.closedStrip; - if(!this.texture.closedStrip && this.texture.symmetryStrip) { - this.onSymmetryChange(); - } - }; - this.onSymmetryChange = e => { - if(this.texture.symmetryStrip) { - this.texture.stripPoints = this.getMovableStripPoints(); - } else { - const nbPointsToAdd = this.texture.stripPoints.length - 2; - for(let i = 0; i < nbPointsToAdd; i++) { - this.addStripPoint(); - } - this.texture.closedStrip = true; // Force closedStrip to true - } - - this.texture.symmetryStrip = !this.texture.symmetryStrip; - this.update(); - } - this.startMoving = which => e => { - const startX = e.screenX, - startY = e.screenY; - if (which === 'axis') { - const oldX = this.texture.axis[0], - oldY = this.texture.axis[1]; - const func = e => { - this.texture.axis[0] = (e.screenX - startX) / this.zoomFactor + oldX; - this.texture.axis[1] = (e.screenY - startY) / this.zoomFactor + oldY; - this.update(); - }, func2 = () => { - document.removeEventListener('mousemove', func); - document.removeEventListener('mouseup', func2); - }; - document.addEventListener('mousemove', func); - document.addEventListener('mouseup', func2); - } else if (which === 'point') { - const point = e.item.point, - oldX = point.x, - oldY = point.y; - let hasMoved = false; - const func = e => { - if(!hasMoved && (e.screenX !== startX || e.screenY !== startY)) { - hasMoved = true; - } - point.x = (e.screenX - startX) / this.zoomFactor + oldX; - point.y = (e.screenY - startY) / this.zoomFactor + oldY; - this.update(); - }, func2 = () => { - if(!hasMoved) { - this.removeStripPoint(e); - this.update(); - } - document.removeEventListener('mousemove', func); - document.removeEventListener('mouseup', func2); - }; - document.addEventListener('mousemove', func); - document.addEventListener('mouseup', func2); - } - }; - /** - * Redraws the canvas with the full image, its collision mask, and its slicing grid - */ - this.refreshTextureCanvas = () => { - textureCanvas.width = textureCanvas.img.width; - textureCanvas.height = textureCanvas.img.height; - textureCanvas.x.strokeStyle = "#0ff"; - textureCanvas.x.lineWidth = 1; - textureCanvas.x.globalCompositeOperation = 'source-over'; - textureCanvas.x.clearRect(0, 0, textureCanvas.width, textureCanvas.height); - textureCanvas.x.drawImage(textureCanvas.img, 0, 0); - textureCanvas.x.globalAlpha = 0.5; - if (!this.texture.tiled) { - for (let i = 0, l = Math.min(this.texture.grid[0] * this.texture.grid[1], this.texture.untill || Infinity); i < l; i++) { - let xx = i % this.texture.grid[0], - yy = Math.floor(i / this.texture.grid[0]), - x = this.texture.offx + xx * (this.texture.marginx + this.texture.width), - y = this.texture.offy + yy * (this.texture.marginy + this.texture.height), - w = this.texture.width, - h = this.texture.height; - textureCanvas.x.strokeRect(x, y, w, h); - } - } - if (this.prevShowMask) { - textureCanvas.x.fillStyle = '#ff0'; - if (this.texture.shape === 'rect') { - textureCanvas.x.fillRect( - this.texture.axis[0] - this.texture.left, - this.texture.axis[1] - this.texture.top, - this.texture.right + this.texture.left, - this.texture.bottom + this.texture.top - ); - } else if (this.texture.shape === 'circle') { - textureCanvas.x.beginPath(); - textureCanvas.x.arc(this.texture.axis[0], this.texture.axis[1], this.texture.r, 0, 2 * Math.PI); - textureCanvas.x.fill(); - } else if (this.texture.shape === 'strip' && this.texture.stripPoints.length) { - textureCanvas.x.strokeStyle = '#ff0'; - textureCanvas.x.lineWidth = 3; - textureCanvas.x.beginPath(); - textureCanvas.x.moveTo(this.texture.stripPoints[0].x + this.texture.axis[0], this.texture.stripPoints[0].y + this.texture.axis[1]); - for (let i = 1, l = this.texture.stripPoints.length; i < l; i++) { - textureCanvas.x.lineTo(this.texture.stripPoints[i].x + this.texture.axis[0], this.texture.stripPoints[i].y + this.texture.axis[1]); - } - if (this.texture.closedStrip) { - textureCanvas.x.closePath(); - } - textureCanvas.x.stroke(); - - if(this.texture.symmetryStrip) { - const movablePoints = this.getMovableStripPoints(); - const axisPoint1 = movablePoints[0]; - const axisPoint2 = movablePoints[movablePoints.length - 1]; - - // Draw symmetry axis - textureCanvas.x.strokeStyle = '#f00'; - textureCanvas.x.lineWidth = 3; - textureCanvas.x.beginPath(); - textureCanvas.x.moveTo(axisPoint1.x + this.texture.axis[0], axisPoint1.y + this.texture.axis[1]); - textureCanvas.x.lineTo(axisPoint2.x + this.texture.axis[0], axisPoint2.y + this.texture.axis[1]); - textureCanvas.x.stroke(); - } - } - } - }; - - /** - * Событие сохранения графики - */ - this.textureSave = () => { - if (this.nameTaken) { - // animate the error notice - require('./data/node_requires/jellify')(this.refs.errorNotice); - soundbox.play('Failure'); - return false; - } - this.parent.fillTextureMap(); - glob.modified = true; - this.texture.lastmod = +(new Date()); - this.textureGenPreview(global.projdir + '/img/' + this.texture.origname + '_prev@2.png', 128); - this.textureGenPreview(global.projdir + '/img/' + this.texture.origname + '_prev.png', 64) - .then(() => { - this.parent.editing = false; - this.parent.update(); - }); - }; - - /** - * Генерирует превьюху первого кадра графики - * @returns {Promise} Промис - */ - this.textureGenPreview = function(destination, size) { - return new Promise((accept, decline) => { - var c = document.createElement('canvas'); - let x = this.texture.offx, - y = this.texture.offy, - w = this.texture.width, - h = this.texture.height; - c.x = c.getContext('2d'); - c.width = c.height = size; - c.x.clearRect(0, 0, size, size); - if (w > h) { - k = size / w; - } else { - k = size / h; - } - if (k > 1) k = 1; - c.x.drawImage(textureCanvas.img, - x, y, w, h, - (size - w*k) / 2, (size - h*k) / 2, - w*k, h*k - ); - var data = c.toDataURL().replace(/^data:image\/\w+;base64,/, ''); - var buf = Buffer.from(data, 'base64'); - fs.writeFile(destination, buf, function(err) { - if (err) { - console.log(err); - decline(err); - } else { - accept(destination); - } - }); - }); - }; - - this.changeTexturePreviewColor = e => { - this.changingTexturePreviewColor = !this.changingTexturePreviewColor; - if (this.changingTexturePreviewColor) { - this.oldPreviewColor = this.previewColor; - } - }; - this.updatePreviewColor = (color, evtype) => { - this.previewColor = color; - if (evtype === 'onapply') { - this.changingTexturePreviewColor = false; - } - this.update(); - }; - this.cancelPreviewColor = () => { - this.changingTexturePreviewColor = false; - this.previewColor = this.oldPreviewColor; - this.update(); - }; - this.getMovableStripPoints = () => { - if(!this.texture) return; - if(!this.texture.symmetryStrip) { - return this.texture.stripPoints; - } else { - return this.texture.stripPoints.slice(0, 2 + Math.round((this.texture.stripPoints.length - 2) / 2)); - } - }; - this.getStripSegments = () => { - if(!this.texture) { - return; - } - if (this.texture.shape !== 'strip') { - return; - } - - const points = this.getMovableStripPoints(); - const segs = []; - - for(let i = 0; i < points.length; i++) { - const point1 = points[i]; - const point2 = points[(i + 1) % points.length]; - - const x1 = (point1.x + this.texture.axis[0]) * this.zoomFactor; - const y1 = (point1.y + this.texture.axis[1]) * this.zoomFactor; - const x2 = (point2.x + this.texture.axis[0]) * this.zoomFactor; - const y2 = (point2.y + this.texture.axis[1]) * this.zoomFactor; - const dx = x2 - x1; - const dy = y2 - y1; - - const length = Math.sqrt(dx*dx + dy*dy); - const cssAngle = Math.atan2(dy, dx) * 180 / Math.PI; - - segs.push({ - left: x1, - top: y1, - width: length, - angle: cssAngle - }); - } - - return segs; - }; - this.updateSymmetricalPoints = () => { - if(this.texture && this.texture.symmetryStrip) { - const movablePoints = this.getMovableStripPoints(); - const axisPoint1 = movablePoints[0]; - const axisPoint2 = movablePoints[movablePoints.length - 1]; - for(let i = 1; i < movablePoints.length - 1; i++) { - const j = this.texture.stripPoints.length - i; - const point = movablePoints[i]; - const axisPoint = this.pointToLine(axisPoint1, axisPoint2, point); - this.texture.stripPoints[j] = { - x: 2 * axisPoint.x - point.x, - y: 2 * axisPoint.y - point.y - } - } - } - }; - +texture-editor.panel.view + .flexrow.tall + .column.borderright.tall.column1.flexfix.nogrow.noshrink + .flexfix-body + fieldset + b {voc.name} + br + input.wide(type="text" value="{opts.texture.name}" onchange="{wire('this.texture.name')}") + .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} + label.checkbox + input#texturetiled(type="checkbox" checked="{opts.texture.tiled}" onchange="{wire('this.texture.tiled')}") + span {voc.tiled} + fieldset + b {voc.center} + .flexrow + input.short(type="number" value="{opts.texture.axis[0]}" onchange="{wire('this.texture.axis.0')}" oninput="{wire('this.texture.axis.0')}") + span.center × + input.short(type="number" value="{opts.texture.axis[1]}" onchange="{wire('this.texture.axis.1')}" oninput="{wire('this.texture.axis.1')}") + .flexrow + button.wide.nml(onclick="{textureCenter}") + span {voc.setcenter} + .spacer + button.square.nmr(onclick="{textureIsometrify}" title="{voc.isometrify}") + svg.feather + use(xlink:href="data/icons.svg#map-pin") + fieldset + b {voc.form} + label.checkbox + input(type="radio" name="collisionform" checked="{opts.texture.shape === 'circle'}" onclick="{textureSelectCircle}") + span {voc.round} + label.checkbox + input(type="radio" name="collisionform" checked="{opts.texture.shape === 'rect'}" onclick="{textureSelectRect}") + span {voc.rectangle} + label.checkbox + input(type="radio" name="collisionform" checked="{opts.texture.shape === 'strip'}" onclick="{textureSelectStrip}") + span {voc.strip} + fieldset(if="{opts.texture.shape === 'circle'}") + b {voc.radius} + br + input.wide(type="number" value="{opts.texture.r}" onchange="{wire('this.texture.r')}" oninput="{wire('this.texture.r')}") + fieldset(if="{opts.texture.shape === 'rect'}") + .center + input.short(type="number" value="{opts.texture.top}" onchange="{wire('this.texture.top')}" oninput="{wire('this.texture.top')}") + br + input.short(type="number" value="{opts.texture.left}" onchange="{wire('this.texture.left')}" oninput="{wire('this.texture.left')}") + span × + input.short(type="number" value="{opts.texture.right}" onchange="{wire('this.texture.right')}" oninput="{wire('this.texture.right')}") + br + input.short(type="number" value="{opts.texture.bottom}" onchange="{wire('this.texture.bottom')}" oninput="{wire('this.texture.bottom')}") + button.wide(onclick="{textureFillRect}") + svg.feather + use(xlink:href="data/icons.svg#maximize") + span {voc.fill} + fieldset(if="{opts.texture.shape === 'strip'}") + .flexrow.aStripPointRow(each="{point, ind in getMovableStripPoints()}") + input.short(type="number" value="{point.x}" oninput="{wire('this.texture.stripPoints.'+ ind + '.x')}") + span × + input.short(type="number" value="{point.y}" oninput="{wire('this.texture.stripPoints.'+ ind + '.y')}") + button.square.inline(title="{voc.removePoint}" onclick="{removeStripPoint}") + svg.feather + use(xlink:href="data/icons.svg#minus") + label.checkbox + input(type="checkbox" checked="{opts.texture.closedStrip}" onchange="{onClosedStripChange}" ) + span {voc.closeShape} + label.checkbox + input(type="checkbox" checked="{opts.texture.symmetryStrip}" onchange="{onSymmetryChange}") + span {voc.symmetryTool} + button.wide(onclick="{addStripPoint}") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.addPoint} + fieldset + label.checkbox + input(checked="{prevShowMask}" onchange="{wire('this.prevShowMask')}" type="checkbox") + span {voc.showmask} + .flexfix-footer + button.wide(onclick="{textureSave}" title="Shift+Control+S" data-hotkey="Control+S") + svg.feather + use(xlink:href="data/icons.svg#save") + span {window.languageJSON.common.save} + .texture-editor-anAtlas.tall( + if="{opts.texture}" + style="background-color: {previewColor};" + onmousewheel="{onMouseWheel}" + ) + .texture-editor-aCanvasWrap + canvas.texture-editor-aCanvas(ref="textureCanvas" style="transform: scale({zoomFactor}); image-rendering: {zoomFactor > 1? 'pixelated' : '-webkit-optimize-contrast'}; transform-origin: 0% 0%;") + // This div is needed to cause elements' reflow so the scrollbars update on canvas' size change + div(style="width: {zoomFactor}px; height: {zoomFactor}px;") + .aClicker( + if="{prevShowMask && opts.texture.shape === 'strip' && getStripSegments()}" + each="{seg, ind in getStripSegments()}" + style="left: {seg.left}px; top: {seg.top}px; width: {seg.width}px; transform: translate(0, -50%) rotate({seg.angle}deg);" + title="{voc.addPoint}" + onclick="{addStripPointOnSegment}" + ) + .aDragger( + if="{prevShowMask}" + style="left: {opts.texture.axis[0] * zoomFactor}px; top: {opts.texture.axis[1] * zoomFactor}px; border-radius: 0;" + title="{voc.moveCenter}" + onmousedown="{startMoving('axis')}" + ) + .aDragger( + if="{prevShowMask && opts.texture.shape === 'strip'}" + each="{point, ind in getMovableStripPoints()}" + style="left: {(point.x + texture.axis[0]) * zoomFactor}px; top: {(point.y + texture.axis[1]) * zoomFactor}px;" + title="{voc.movePoint}" + onmousedown="{startMoving('point')}" + ) + .textureview-tools + .toright + label.file(title="{voc.replacetexture}") + input(type="file" ref="textureReplacer" accept=".png,.jpg,.jpeg,.bmp,.gif" onchange="{textureReplace}") + .button.inline + svg.feather + use(xlink:href="data/icons.svg#folder") + span {voc.replacetexture} + .button.inline(title="{voc.reimport}" if="{opts.texture.source}" onclick="{reimport}") + svg.feather + use(xlink:href="data/icons.svg#refresh-ccw") + .textureview-zoom + div.button-stack.inlineblock + button#texturezoom25.inline(onclick="{textureToggleZoom(0.25)}" class="{active: zoomFactor === 0.25}") 25% + button#texturezoom50.inline(onclick="{textureToggleZoom(0.5)}" class="{active: zoomFactor === 0.5}") 50% + button#texturezoom100.inline(onclick="{textureToggleZoom(1)}" class="{active: zoomFactor === 1}") 100% + button#texturezoom200.inline(onclick="{textureToggleZoom(2)}" class="{active: zoomFactor === 2}") 200% + button#texturezoom400.inline(onclick="{textureToggleZoom(4)}" class="{active: zoomFactor === 4}") 400% + .column.column2.borderleft.tall.flexfix.nogrow.noshrink(show="{!opts.texture.tiled}") + .flexfix-body + fieldset + .flexrow + div + b {voc.cols} + br + input.wide(type="number" value="{opts.texture.grid[0]}" onchange="{wire('this.texture.grid.0')}" oninput="{wire('this.texture.grid.0')}") + span   + div + b {voc.rows} + br + input.wide(type="number" value="{opts.texture.grid[1]}" onchange="{wire('this.texture.grid.1')}" oninput="{wire('this.texture.grid.1')}") + .flexrow + div + b {voc.width} + br + input.wide(type="number" value="{opts.texture.width}" onchange="{wire('this.texture.width')}" oninput="{wire('this.texture.width')}") + span   + div + b {voc.height} + br + input.wide(type="number" value="{opts.texture.height}" onchange="{wire('this.texture.height')}" oninput="{wire('this.texture.height')}") + .flexrow + div + b {voc.marginx} + br + input.wide(type="number" value="{opts.texture.marginx}" onchange="{wire('this.texture.marginx')}" oninput="{wire('this.texture.marginx')}") + span   + div + b {voc.marginy} + br + input.wide(type="number" value="{opts.texture.marginy}" onchange="{wire('this.texture.marginy')}" oninput="{wire('this.texture.marginy')}") + .flexrow + div + b {voc.offx} + br + input.wide(type="number" value="{opts.texture.offx}" onchange="{wire('this.texture.offx')}" oninput="{wire('this.texture.offx')}") + span   + div + b {voc.offy} + br + input.wide(type="number" value="{opts.texture.offy}" onchange="{wire('this.texture.offy')}" oninput="{wire('this.texture.offy')}") + fieldset + b {voc.frames} + br + input#textureframes.wide(type="number" value="{opts.texture.untill}" onchange="{wire('this.texture.untill')}" oninput="{wire('this.texture.untill')}") + fieldset + b + span {voc.padding} + hover-hint(text="{voc.paddingNotice}") + br + input.wide(type="number" min="0" max="128" step="1" value="{opts.texture.padding}" onchange="{wire('this.texture.padding')}") + .preview.bordertop.flexfix-footer + #preview(ref="preview" style="background-color: {previewColor};") + canvas(ref="grprCanvas") + .flexrow + button#textureplay.square.inline(onclick="{previewPlayPause}") + svg.feather + use(xlink:href="data/icons.svg#{prevPlaying? 'pause' : 'play'}") + span(ref="textureviewframe") 0 / 1 + .filler + button#textureviewback.square.inline(onclick="{previewBack}") + svg.feather + use(xlink:href="data/icons.svg#skip-back") + button#textureviewnext.square.inline.nmr(onclick="{previewNext}") + svg.feather + use(xlink:href="data/icons.svg#skip-forward") + .flexrow + b {voc.speed} + .filler + input#grahpspeed.short(type="number" min="1" value="{prevSpeed}" onchange="{wire('this.prevSpeed')}" oninput="{wire('this.prevSpeed')}") + .relative + button#texturecolor.inline.wide(onclick="{changePreviewBg}") + svg.feather + use(xlink:href="data/icons.svg#droplet") + span {voc.bgcolor} + input.color.rgb#previewbgcolor + + color-picker( + ref="previewBackgroundColor" if="{changingPreviewBg}" + hidealpha="true" + color="{previewColor}" onapply="{updatePreviewColor}" onchanged="{updatePreviewColor}" oncancel="{cancelPreviewColor}" + ) + script. + const path = require('path'), + fs = require('fs-extra'); + const glob = require('./data/node_requires/glob'); + this.namespace = 'textureview'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + + this.nameTaken = false; + this.prevPlaying = true; + this.prevPos = 0; + this.prevSpeed = 10; + this.prevShowMask = true; + this.previewColor = localStorage.UItheme === 'Day' ? '#ffffff' : '#08080D'; + this.zoomFactor = 1; + + var textureCanvas, grprCanvas; + + this.on('mount', () => { + textureCanvas = this.refs.textureCanvas; + grprCanvas = this.refs.grprCanvas; + textureCanvas.x = textureCanvas.getContext('2d'); + grprCanvas.x = grprCanvas.getContext('2d'); + var texture = this.texture = this.opts.texture; + var img = document.createElement('img'); + img.onload = () => { + textureCanvas.img = img; + this.update(); + setTimeout(() => { + this.launchTexturePreview(); + }, 0); + }; + img.onerror = e => { + alertify.error(window.languageJSON.textureview.corrupted); + console.error(e); + this.textureSave(); + }; + img.src = path.join('file://', global.projdir, '/img/', texture.origname) + '?' + Math.random(); + }); + this.on('update', () => { + const {textures} = global.currentProject; + if (textures.find(texture => this.texture.name === texture.name && this.texture !== texture)) { + this.nameTaken = true; + } else { + this.nameTaken = false; + } + this.updateSymmetricalPoints(); + }); + this.on('updated', () => { + this.refreshTextureCanvas(); + }); + this.on('unmount', () => { + if (this.prevPlaying) { // вырубаем анимацию превью, если редактор был закрыт + this.stopTexturePreview(); + } + }); + + this.textureReplace = () => { + const val = this.refs.textureReplacer.files[0].path; + if (/\.(jpg|gif|png|jpeg)/gi.test(val)) { + this.loadImg( + this.texture.uid, + val, + global.projdir + '/img/i' + this.texture.uid + path.extname(val) + ); + this.texture.source = val; + } else { + alertify.error(window.languageJSON.common.wrongFormat); + } + this.refs.textureReplacer.value = ''; + }; + this.reimport = () => { + this.loadImg( + this.texture.uid, + this.texture.source, + global.projdir + '/img/i' + this.texture.uid + path.extname(this.texture.source) + ); + }; + + /** + * Загружает изображение в редактор и генерирует квадратную превьюху из исходного изображения + * @param {Number} uid Идентификатор изображения + * @param {String} filename Путь к исходному изображению + * @param {Sting} dest Путь к изображению в папке проекта + */ + this.loadImg = (uid, filename, dest) => { + fs.copy(filename, dest, e => { + if (e) { + throw e; + } + const image = document.createElement('img'); + image.onload = () => { + this.texture.imgWidth = image.width; + this.texture.imgHeight = image.height; + if (this.texture.tiled || ( + this.texture.grid[0] === 1 && + this.texture.grid[1] === 1 && + this.texture.offx === 0 && + this.texture.offy === 0 + )) { + this.texture.width = this.texture.imgWidth; + this.texture.height = this.texture.imgHeight; + } + this.texture.origname = path.basename(dest); + textureCanvas.img = image; + this.texture.lastmod = Number(new Date()); + + const {imgGenPreview} = require('./data/node_requires/resources/textures'); + imgGenPreview(dest, dest + '_prev.png', 64, () => { + this.update(); + }); + imgGenPreview(dest, dest + '_prev@2.png', 128); + setTimeout(() => { + this.refreshTextureCanvas(); + this.parent.fillTextureMap(); + this.launchTexturePreview(); + }, 0); + }; + image.onerror = e => { + alertify.error(e); + }; + image.src = 'file://' + dest + '?' + Math.random(); + }); + }; + + this.textureToggleZoom = zoom => () => { + this.zoomFactor = zoom; + }; + /** Change zoomFactor on mouse wheel roll */ + this.onMouseWheel = e => { + if (e.wheelDelta > 0) { + // in + if (this.zoomFactor === 2) { + this.zoomFactor = 4; + } else if (this.zoomFactor === 1) { + this.zoomFactor = 2; + } else if (this.zoomFactor === 0.5) { + this.zoomFactor = 1; + } else if (this.zoomFactor === 0.25) { + this.zoomFactor = 0.5; + } + } else if (this.zoomFactor === 4) { // out + this.zoomFactor = 2; + } else if (this.zoomFactor === 2) { + this.zoomFactor = 1; + } else if (this.zoomFactor === 1) { + this.zoomFactor = 0.5; + } else if (this.zoomFactor === 0.5) { + this.zoomFactor = 0.25; + } + e.preventDefault(); + this.update(); + }; + + /** + * Установить ось вращения на центр изображения + */ + this.textureCenter = () => { + const {texture} = this; + texture.axis[0] = Math.floor(texture.width / 2); + texture.axis[1] = Math.floor(texture.height / 2); + }; + /** + * Заполнить всё изображение маской-квадратом + */ + this.textureFillRect = () => { + const {texture} = this; + texture.left = Math.floor(texture.axis[0]); + texture.top = Math.floor(texture.axis[1]); + texture.right = Math.ceil(texture.width - texture.axis[0]); + texture.bottom = Math.ceil(texture.height - texture.axis[1]); + }; + this.textureIsometrify = () => { + const {texture} = this; + texture.axis[0] = Math.floor(texture.width / 2); + texture.axis[1] = texture.height; + this.textureFillRect(); + }; + /** + * Запустить предпросмотр анимации + */ + this.previewPlayPause = () => { + if (this.prevPlaying) { + this.stopTexturePreview(); + } else { + this.launchTexturePreview(); + } + this.prevPlaying = !this.prevPlaying; + }; + /** + * Отступить на шаг назад в предпросмотре анимации + */ + this.previewBack = () => { + this.prevPos--; + const {texture} = this; + const total = texture.untill === 0 ? + texture.grid[0] * texture.grid[1] : + Math.min(texture.grid[0] * texture.grid[1], texture.untill); + if (this.prevPos < 0) { + this.prevPos = texture.untill === 0 ? texture.grid[0] * texture.grid[1] : total - 0; + } + this.refreshPreviewCanvas(); + }; + /** + * Шагнуть на кадр вперёд в предпросмотре анимации + */ + this.previewNext = () => { + this.prevPos++; + const {texture} = this; + const total = texture.untill === 0 ? + texture.grid[0] * texture.grid[1] : + Math.min(texture.grid[0] * texture.grid[1], texture.untill); + if (this.prevPos >= total) { + this.prevPos = 0; + } + this.refreshPreviewCanvas(); + }; + this.refreshPreviewCanvas = () => { + const xx = this.prevPos % this.texture.grid[0], + yy = Math.floor(this.prevPos / this.texture.grid[0]), + x = this.texture.offx + xx * (this.texture.marginx + this.texture.width), + y = this.texture.offy + yy * (this.texture.marginy + this.texture.height), + w = this.texture.width, + h = this.texture.height; + grprCanvas.width = w; + grprCanvas.height = h; + + grprCanvas.x.clearRect(0, 0, grprCanvas.width, grprCanvas.height); + grprCanvas.x.drawImage( + textureCanvas.img, + x, y, w, h, + 0, 0, w, h + ); + // shape + if (this.prevShowMask) { + grprCanvas.x.globalAlpha = 0.5; + grprCanvas.x.fillStyle = '#ff0'; + if (this.texture.shape === 'rect') { + grprCanvas.x.fillRect( + this.texture.axis[0] - this.texture.left, + this.texture.axis[1] - this.texture.top, + this.texture.right + this.texture.left, + this.texture.bottom + this.texture.top + ); + } else if (this.texture.shape === 'circle') { + grprCanvas.x.beginPath(); + grprCanvas.x.arc( + this.texture.axis[0], this.texture.axis[1], + this.texture.r, + 0, 2 * Math.PI + ); + grprCanvas.x.fill(); + } else if (this.texture.shape === 'strip' && this.texture.stripPoints.length) { + grprCanvas.x.strokeStyle = '#ff0'; + grprCanvas.x.lineWidth = 3; + grprCanvas.x.beginPath(); + grprCanvas.x.moveTo( + this.texture.stripPoints[0].x + this.texture.axis[0], + this.texture.stripPoints[0].y + this.texture.axis[1] + ); + for (let i = 1, l = this.texture.stripPoints.length; i < l; i++) { + grprCanvas.x.lineTo( + this.texture.stripPoints[i].x + this.texture.axis[0], + this.texture.stripPoints[i].y + this.texture.axis[1] + ); + } + if (this.texture.closedStrip) { + grprCanvas.x.closePath(); + } + grprCanvas.x.stroke(); + } + grprCanvas.x.globalAlpha = 1; + grprCanvas.x.fillStyle = '#f33'; + grprCanvas.x.beginPath(); + grprCanvas.x.arc(this.texture.axis[0], this.texture.axis[1], 3, 0, 2 * Math.PI); + grprCanvas.x.fill(); + } + }; + /** + * Stops the animated preview + */ + this.stopTexturePreview = () => { + window.clearTimeout(this.prevTime); + }; + /** + * Starts the preview of a framed animation + */ + this.launchTexturePreview = () => { + if (this.prevTime) { + window.clearTimeout(this.prevTime); + } + this.prevPos = 0; + this.stepTexturePreview(); + }; + /** + * Шаг анимации в предпросмотре. Выполняет рендер. Записывает таймер следующего шага в this.prevTime + */ + this.stepTexturePreview = () => { + const {texture} = this; + this.prevTime = window.setTimeout(() => { + const total = Math.min( + texture.untill === 0 ? Infinity : texture.untill, + texture.grid[0] * texture.grid[1] + ); + this.prevPos++; + if (this.prevPos >= total) { + this.prevPos = 0; + } + this.refs.textureviewframe.innerHTML = `${this.prevPos} / ${total}`; + if (!this.texture.tiled) { + this.refreshPreviewCanvas(); + } + this.stepTexturePreview(); + }, 1000 / this.prevSpeed); + }; + + /** + * Переключает тип маски на круг и выставляет начальные параметры + */ + this.textureSelectCircle = function textureSelectCircle() { + this.texture.shape = 'circle'; + if (!('r' in this.texture) || this.texture.r === 0) { + this.texture.r = Math.min( + Math.floor(this.texture.width / 2), + Math.floor(this.texture.height / 2) + ); + } + }; + /** + * Переключает тип маски на прямоугольник и выставляет начальные параметры + */ + this.textureSelectRect = function textureSelectRect() { + this.texture.shape = 'rect'; + this.textureFillRect(); + }; + /** + * Переключает тип маски на ломаную/многоугольник и выставляет начальные параметры + */ + this.textureSelectStrip = function textureSelectStrip() { + this.texture.shape = 'strip'; + this.texture.stripPoints = this.texture.stripPoints || []; + if (!this.texture.stripPoints.length) { + const twoPi = Math.PI * 2; + this.texture.closedStrip = true; + for (let i = 0; i < 5; i++) { + this.texture.stripPoints.push({ + x: Math.round(Math.sin(twoPi / 5 * i) * this.texture.width / 2), + y: -Math.round(Math.cos(twoPi / 5 * i) * this.texture.height / 2) + }); + } + } + }; + this.removeStripPoint = function removeStripPoint(e) { + if (this.texture.symmetryStrip) { + // Remove an extra point + this.texture.stripPoints.pop(); + } + this.texture.stripPoints.splice(e.item.ind, 1); + }; + this.addStripPoint = function addStripPoint() { + this.texture.stripPoints.push({ + x: 0, + y: 16 + }); + }; + this.addStripPointOnSegment = e => { + const {top, left} = textureCanvas.getBoundingClientRect(); + this.texture.stripPoints.splice(e.item.ind + 1, 0, { + x: (e.pageX - left) / this.zoomFactor - this.texture.axis[0], + y: (e.pageY - top) / this.zoomFactor - this.texture.axis[1] + }); + if (this.texture.symmetryStrip) { + // Add an extra point (the symetrical point) + this.addStripPoint(); + } + }; + this.pointToLine = (linePoint1, linePoint2, point) => { + const dlx = linePoint2.x - linePoint1.x; + const dly = linePoint2.y - linePoint1.y; + const lineLength = Math.sqrt(dlx * dlx + dly * dly); + const lineAngle = Math.atan2(dly, dlx); + + const dpx = point.x - linePoint1.x; + const dpy = point.y - linePoint1.y; + const toPointLength = Math.sqrt(dpx * dpx + dpy * dpy); + const toPointAngle = Math.atan2(dpy, dpx); + + const distance = toPointLength * Math.cos(toPointAngle - lineAngle); + + return { + x: linePoint1.x + distance * dlx / lineLength, + y: linePoint1.y + distance * dly / lineLength + }; + }; + this.onClosedStripChange = () => { + this.texture.closedStrip = !this.texture.closedStrip; + if (!this.texture.closedStrip && this.texture.symmetryStrip) { + this.onSymmetryChange(); + } + }; + this.onSymmetryChange = () => { + if (this.texture.symmetryStrip) { + this.texture.stripPoints = this.getMovableStripPoints(); + } else { + const nbPointsToAdd = this.texture.stripPoints.length - 2; + for (let i = 0; i < nbPointsToAdd; i++) { + this.addStripPoint(); + } + this.texture.closedStrip = true; // Force closedStrip to true + } + + this.texture.symmetryStrip = !this.texture.symmetryStrip; + this.update(); + }; + this.startMoving = which => e => { + const startX = e.screenX, + startY = e.screenY; + if (which === 'axis') { + const [oldX, oldY] = this.texture.axis; + const func = e => { + this.texture.axis[0] = (e.screenX - startX) / this.zoomFactor + oldX; + this.texture.axis[1] = (e.screenY - startY) / this.zoomFactor + oldY; + this.update(); + }; + const func2 = () => { + document.removeEventListener('mousemove', func); + document.removeEventListener('mouseup', func2); + }; + document.addEventListener('mousemove', func); + document.addEventListener('mouseup', func2); + } else if (which === 'point') { + const {point} = e.item, + oldX = point.x, + oldY = point.y; + let hasMoved = false; + const func = e => { + if (!hasMoved && (e.screenX !== startX || e.screenY !== startY)) { + hasMoved = true; + } + point.x = (e.screenX - startX) / this.zoomFactor + oldX; + point.y = (e.screenY - startY) / this.zoomFactor + oldY; + this.update(); + }; + const func2 = () => { + if (!hasMoved) { + this.removeStripPoint(e); + this.update(); + } + document.removeEventListener('mousemove', func); + document.removeEventListener('mouseup', func2); + }; + document.addEventListener('mousemove', func); + document.addEventListener('mouseup', func2); + } + }; + /** + * Redraws the canvas with the full image, its collision mask, and its slicing grid + */ + this.refreshTextureCanvas = () => { + textureCanvas.width = textureCanvas.img.width; + textureCanvas.height = textureCanvas.img.height; + textureCanvas.x.strokeStyle = '#0ff'; + textureCanvas.x.lineWidth = 1; + textureCanvas.x.globalCompositeOperation = 'source-over'; + textureCanvas.x.clearRect(0, 0, textureCanvas.width, textureCanvas.height); + textureCanvas.x.drawImage(textureCanvas.img, 0, 0); + textureCanvas.x.globalAlpha = 0.5; + if (!this.texture.tiled) { + const l = Math.min( + this.texture.grid[0] * this.texture.grid[1], + this.texture.untill || Infinity + ); + for (let i = 0; i < l; i++) { + const xx = i % this.texture.grid[0], + yy = Math.floor(i / this.texture.grid[0]), + x = this.texture.offx + xx * (this.texture.marginx + this.texture.width), + y = this.texture.offy + yy * (this.texture.marginy + this.texture.height), + w = this.texture.width, + h = this.texture.height; + textureCanvas.x.strokeRect(x, y, w, h); + } + } + if (this.prevShowMask) { + textureCanvas.x.fillStyle = '#ff0'; + if (this.texture.shape === 'rect') { + textureCanvas.x.fillRect( + this.texture.axis[0] - this.texture.left, + this.texture.axis[1] - this.texture.top, + this.texture.right + this.texture.left, + this.texture.bottom + this.texture.top + ); + } else if (this.texture.shape === 'circle') { + textureCanvas.x.beginPath(); + textureCanvas.x.arc( + this.texture.axis[0], + this.texture.axis[1], + this.texture.r, + 0, 2 * Math.PI + ); + textureCanvas.x.fill(); + } else if (this.texture.shape === 'strip' && this.texture.stripPoints.length) { + textureCanvas.x.strokeStyle = '#ff0'; + textureCanvas.x.lineWidth = 3; + textureCanvas.x.beginPath(); + textureCanvas.x.moveTo( + this.texture.stripPoints[0].x + this.texture.axis[0], + this.texture.stripPoints[0].y + this.texture.axis[1] + ); + for (let i = 1, l = this.texture.stripPoints.length; i < l; i++) { + textureCanvas.x.lineTo( + this.texture.stripPoints[i].x + this.texture.axis[0], + this.texture.stripPoints[i].y + this.texture.axis[1] + ); + } + if (this.texture.closedStrip) { + textureCanvas.x.closePath(); + } + textureCanvas.x.stroke(); + + if (this.texture.symmetryStrip) { + const movablePoints = this.getMovableStripPoints(); + const [axisPoint1] = movablePoints; + const axisPoint2 = movablePoints[movablePoints.length - 1]; + + // Draw symmetry axis + textureCanvas.x.strokeStyle = '#f00'; + textureCanvas.x.lineWidth = 3; + textureCanvas.x.beginPath(); + textureCanvas.x.moveTo( + axisPoint1.x + this.texture.axis[0], + axisPoint1.y + this.texture.axis[1] + ); + textureCanvas.x.lineTo( + axisPoint2.x + this.texture.axis[0], + axisPoint2.y + this.texture.axis[1] + ); + textureCanvas.x.stroke(); + } + } + } + }; + + /** + * Событие сохранения графики + */ + this.textureSave = () => { + if (this.nameTaken) { + // animate the error notice + require('./data/node_requires/jellify')(this.refs.errorNotice); + soundbox.play('Failure'); + return false; + } + this.parent.fillTextureMap(); + glob.modified = true; + this.texture.lastmod = Number(new Date()); + this.textureGenPreview(global.projdir + '/img/' + this.texture.origname + '_prev@2.png', 128); + this.textureGenPreview(global.projdir + '/img/' + this.texture.origname + '_prev.png', 64) + .then(() => { + this.parent.editing = false; + this.parent.update(); + }); + return true; + }; + + /** + * Генерирует превьюху первого кадра графики + * @returns {Promise} Промис + */ + this.textureGenPreview = function textureGenPreview(destination, size) { + return new Promise((accept, decline) => { + var c = document.createElement('canvas'); + const x = this.texture.offx, + y = this.texture.offy, + w = this.texture.width, + h = this.texture.height; + let k; + c.x = c.getContext('2d'); + c.width = c.height = size; + c.x.clearRect(0, 0, size, size); + if (w > h) { + k = size / w; + } else { + k = size / h; + } + if (k > 1) { + k = 1; + } + c.x.drawImage( + textureCanvas.img, + x, y, w, h, + (size - w * k) / 2, (size - h * k) / 2, + w * k, h * k + ); + var thumbnailBase64 = c.toDataURL().replace(/^data:image\/\w+;base64,/, ''); + var buf = Buffer.from(thumbnailBase64, 'base64'); + fs.writeFile(destination, buf, err => { + if (err) { + console.error(err); + decline(err); + } else { + accept(destination); + } + }); + }); + }; + + + this.changePreviewBg = () => { + this.changingPreviewBg = !this.changingPreviewBg; + if (this.changingPreviewBg) { + this.oldPreviewColor = this.previewColor; + } + }; + this.updatePreviewColor = (color, evtype) => { + this.previewColor = color; + if (evtype === 'onapply') { + this.changingPreviewBg = false; + } + this.update(); + }; + this.cancelPreviewColor = () => { + this.changingPreviewBg = false; + this.previewColor = this.oldPreviewColor; + this.update(); + }; + this.getMovableStripPoints = () => { + if (!this.texture) { + return false; + } + const points = this.texture.stripPoints; + if (!this.texture.symmetryStrip) { + return points; + } + return points.slice(0, 2 + Math.round((points.length - 2) / 2)); + }; + this.getStripSegments = () => { + if (!this.texture) { + return false; + } + if (this.texture.shape !== 'strip') { + return false; + } + + const points = this.getMovableStripPoints(); + const segs = []; + + for (let i = 0; i < points.length; i++) { + const point1 = points[i]; + const point2 = points[(i + 1) % points.length]; + + const x1 = (point1.x + this.texture.axis[0]) * this.zoomFactor; + const y1 = (point1.y + this.texture.axis[1]) * this.zoomFactor; + const x2 = (point2.x + this.texture.axis[0]) * this.zoomFactor; + const y2 = (point2.y + this.texture.axis[1]) * this.zoomFactor; + const dx = x2 - x1; + const dy = y2 - y1; + + const length = Math.sqrt(dx * dx + dy * dy); + const cssAngle = Math.atan2(dy, dx) * 180 / Math.PI; + + segs.push({ + left: x1, + top: y1, + width: length, + angle: cssAngle + }); + } + + return segs; + }; + this.updateSymmetricalPoints = () => { + if (this.texture && this.texture.symmetryStrip) { + const movablePoints = this.getMovableStripPoints(); + const [axisPoint1] = movablePoints; + const axisPoint2 = movablePoints[movablePoints.length - 1]; + for (let i = 1; i < movablePoints.length - 1; i++) { + const j = this.texture.stripPoints.length - i; + const point = movablePoints[i]; + const axisPoint = this.pointToLine(axisPoint1, axisPoint2, point); + this.texture.stripPoints[j] = { + x: 2 * axisPoint.x - point.x, + y: 2 * axisPoint.y - point.y + }; + } + } + }; + diff --git a/src/riotTags/textures-panel.tag b/src/riotTags/textures-panel.tag index cb564d778..1d01b6735 100644 --- a/src/riotTags/textures-panel.tag +++ b/src/riotTags/textures-panel.tag @@ -1,367 +1,363 @@ -textures-panel.panel.view - .flexfix.tall - div - asset-viewer( - collection="{global.currentProject.textures}" - contextmenu="{showTexturePopup}" - vocspace="texture" - namespace="textures" - click="{openTexture}" - thumbnails="{thumbnails}" - ref="textures" - ) - label.file.flexfix-header - input(type="file" multiple - accept=".png,.jpg,.jpeg,.bmp,.gif,.json" - onchange="{parent.textureImport}") - .button - svg.feather - use(xlink:href="data/icons.svg#download") - span {voc.import} - asset-viewer( - collection="{global.currentProject.skeletons}" - contextmenu="{showSkeletonPopup}" - vocspace="texture" - namespace="skeletons" - thumbnails="{thumbnails}" - ref="skeletons" - ) - h2 - span {voc.skeletons} - docs-shortcut(path="/skeletal-animation.html") - label.file.flexfix-header - input(type="file" multiple - accept=".json" - onchange="{parent.textureImport}") - .button - svg.feather - use(xlink:href="data/icons.svg#download") - span {voc.import} - - .aDropzone(if="{dropping}") - .middleinner - svg.feather - use(xlink:href="data/icons.svg#download") - h2 {languageJSON.common.fastimport} - input(type="file" multiple - accept=".png,.jpg,.jpeg,.bmp,.gif,.json" - onchange="{textureImport}") - - texture-editor(if="{editing}" texture="{currentTexture}") - context-menu(menu="{textureMenu}" ref="textureMenu") - script. - const fs = require('fs-extra'), - path = require('path'); - const glob = require('./data/node_requires/glob'); - const generateGUID = require('./data/node_requires/generateGUID'); - this.namespace = 'texture'; - this.mixin(window.riotVoc); - this.editing = false; - this.dropping = false; - - const {getTexturePreview} = require('./data/node_requires/resources/textures'); - // this.thumbnails = texture => `file://${global.projdir}/img/${texture.origname}_prev.png?cache=${texture.lastmod}`; - this.thumbnails = getTexturePreview; - - this.fillTextureMap = () => { - glob.texturemap = {}; - global.currentProject.textures.forEach(texture => { - var img = document.createElement('img'); - glob.texturemap[texture.uid] = img; - img.g = texture; - img.src = 'file://' + global.projdir + '/img/' + texture.origname + '?' + texture.lastmod; - }); - var img = document.createElement('img'); - glob.texturemap[-1] = img; - img.g = { - width: 32, - height: 32, - offx: 0, - offy: 0, - grid: [1, 1], - axis: [16, 16], - marginx: 0, - marginy: 0, - imgWidth: 32, - imgHeight: 32, - closedStrip: true - } - img.src = 'data/img/unknown.png'; - }; - - this.setUpPanel = e => { - this.fillTextureMap(); - this.refs.textures.updateList(); - this.refs.skeletons.updateList(); - this.searchResults = null; - this.editing = false; - this.dropping = false; - this.currentTexture = null; - this.update(); - }; - this.updateTextureData = () => { - this.refs.textures.updateList(); - this.refs.skeletons.updateList(); - this.update(); - this.fillTextureMap(); - }; - - window.signals.on('projectLoaded', this.setUpPanel); - window.signals.on('textureImported', this.updateTextureData); - this.on('mount', this.setUpPanel); - this.on('unmount', () => { - window.signals.off('projectLoaded', this.setUpPanel); - window.signals.off('textureImported', this.updateTextureData); - }); - - /** - * An event fired when user attempts to add files from a file manager (by clicking an "Import" button) - */ - this.textureImport = e => { // input[type="file"] - const {importImageToTexture} = require('./data/node_requires/resources/textures'); - const files = [...e.target.files].map(file => file.path); - for (let i = 0; i < files.length; i++) { - if (/\.(jpg|gif|png|jpeg)/gi.test(files[i])) { - let id = generateGUID(); - importImageToTexture(files[i]); - } else if (/_ske\.json/i.test(files[i])) { - let id = generateGUID(); - this.loadSkeleton( - id, - files[i], - global.projdir + '/img/skdb' + id + '_ske.json' - ); - } - } - e.srcElement.value = ''; - this.dropping = false; - e.preventDefault(); - }; - - this.loadSkeleton = (uid, filename, dest) => { - fs.copy(filename, dest) - .then(() => fs.copy(filename.replace('_ske.json', '_tex.json'), dest.replace('_ske.json', '_tex.json'))) - .then(() => fs.copy(filename.replace('_ske.json', '_tex.png'), dest.replace('_ske.json', '_tex.png'))) - .then(() => { - global.currentProject.skeletons.push({ - name: path.basename(filename).replace('_ske.json', ''), - origname: path.basename(dest), - from: 'dragonbones', - uid: uid - }) - this.skelGenPreview(dest, dest + '_prev.png', [64, 128]) - .then(dataUrl => { - this.refs.skeletons.updateList(); - this.update(); - }); - }); - }; - - /** - * Generates a square preview for a given skeleton - * @param {String} source Path to the source _ske.json file - * @param {String} destFile Path to the destinating image - * @param {Number} size Size of the square thumbnail, in pixels - * @returns {Promise} Resolves after creating a thumbnail. On success, passes data-url of the created thumbnail. - */ - this.skelGenPreview = (source, destFile, sizes) => { - const loader = new PIXI.loaders.Loader(), - dbf = dragonBones.PixiFactory.factory; - const slice = 'file://' + source.replace('_ske.json', ''); - return new Promise((resolve, reject) => { - loader.add(`${slice}_ske.json`, `${slice}_ske.json`) - .add(`${slice}_tex.json`, `${slice}_tex.json`) - .add(`${slice}_tex.png`, `${slice}_tex.png`); - loader.load(() => { - dbf.parseDragonBonesData(loader.resources[`${slice}_ske.json`].data); - dbf.parseTextureAtlasData(loader.resources[`${slice}_tex.json`].data, loader.resources[`${slice}_tex.png`].texture); - const skel = dbf.buildArmatureDisplay('Armature', loader.resources[`${slice}_ske.json`].data.name); - const promises = sizes.map(size => new Promise((resolve, reject) => { - const app = new PIXI.Application(); - const base64 = app.renderer.plugins.extract.base64(skel) - const data = base64.replace(/^data:image\/\w+;base64,/, ''); - const buf = new Buffer(data, 'base64'); - const stream = fs.createWriteStream(destFile); - stream.on('finish', () => { - setTimeout(() => { // WHY THE HECK I EVER NEED THIS?! - resolve(destFile); - }, 100); - }); - stream.on('error', err => { - reject(err); - }); - stream.end(buf); - })); - Promise.all(promises) - .then(() => { - delete dbf._dragonBonesDataMap[loader.resources[`${slice}_ske.json`].data.name]; - delete dbf._textureAtlasDataMap[loader.resources[`${slice}_ske.json`].data.name]; - }) - .then(resolve) - .catch(reject); - }); - }); - }; - - // Creates a context menu that will appear on RMB click on texture cards - this.textureMenu = { - opened: false, - items: [{ - label: languageJSON.common.open, - click: e => { - if (this.currentTextureType === 'skeleton') { - this.openSkeleton(this.currentTexture)(); - } else { - this.openTexture(this.currentTexture)(); - } - this.update(); - } - }, { - label: languageJSON.common.copyName, - click: e => { - nw.Clipboard.get().set(this.currentTexture.name, 'text'); - } - }, { - label: window.languageJSON.common.rename, - click: e => { - alertify - .defaultValue(this.currentTexture.name) - .prompt(window.languageJSON.common.newname) - .then(e => { - if (e.inputValue && e.inputValue != '' && e.buttonClicked !== 'cancel') { - this.currentTexture.name = e.inputValue; - this.update(); - } - }); - } - }, { - type: 'separator' - }, { - label: window.languageJSON.common.delete, - click: e => { - alertify - .okBtn(window.languageJSON.common.delete) - .cancelBtn(window.languageJSON.common.cancel) - .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.currentTexture.name)) - .then(e => { - if (e.buttonClicked === 'ok') { - if (this.currentTextureType === 'skeleton') { - global.currentProject.skeletons.splice(this.currentTextureId, 1); - } else { - for (const type of global.currentProject.types) { - if (type.texture === this.currentTexture.uid) { - type.texture = -1; - } - } - for (const room of global.currentProject.rooms) { - if ('tiles' in room) { - for (const layer of room.tiles) { - let i = 0; - while (i < layer.tiles.length) { - if (layer.tiles[i].texture === this.currentTexture.uid) { - layer.tiles.splice(i, 1); - } else { - i++; - } - } - } - } - if ('backgrounds' in room) { - let i = 0; - while (i < room.backgrounds.length) { - if (room.backgrounds[i].texture === this.currentTexture.uid) { - room.backgrounds.splice(i, 1); - } else { - i++; - } - } - } - } - for (const tandem of global.currentProject.emitterTandems) { - for (const emitter of tandem.emitters) { - if (emitter.texture === this.currentTexture.uid) { - emitter.texture = -1; - } - } - } - if (global.currentProject.settings.icon === this.currentTexture.uid) { - delete global.currentProject.settings.icon; - } - global.currentProject.textures.splice(this.currentTextureId, 1); - } - this.refs.textures.updateList(); - this.refs.skeletons.updateList(); - this.update(); - alertify - .okBtn(window.languageJSON.common.ok) - .cancelBtn(window.languageJSON.common.cancel); - } - }); - } - }] - }; - /** - * Shows the context menu created above - */ - this.showTexturePopup = (texture, isSkeleton) => e => { - this.currentTextureType = isSkeleton? 'skeleton' : 'texture'; - if (isSkeleton) { - this.currentTextureId = global.currentProject.skeletons.indexOf(texture); - } else { - this.currentTextureId = global.currentProject.textures.indexOf(texture); - } - this.currentTexture = texture; - this.refs.textureMenu.popup(e.clientX, e.clientY); - e.preventDefault(); - }; - this.showSkeletonPopup = skel => e => { - this.showTexturePopup(skel, true)(e); - }; - - /** - * Opens an editor for the given texture - */ - this.openTexture = texture => e => { - this.currentTexture = texture; - this.currentTextureId = global.currentProject.textures.indexOf(texture); - this.editing = true; - }; - this.openSkeleton = skel => e => { - - }; - - /* - * drag-n-drop handling - */ - var dragTimer; - this.onDragOver = e => { - var dt = e.dataTransfer; - if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('Files'))) { - this.dropping = true; - this.update(); - window.clearTimeout(dragTimer); - } - e.preventDefault(); - e.stopPropagation(); - }; - this.onDrop = e => { - e.stopPropagation(); - }; - this.onDragLeave = e => { - dragTimer = window.setTimeout(() => { - this.dropping = false; - this.update() - }, 25); - e.preventDefault(); - e.stopPropagation(); - }; - this.on('mount', () => { - document.addEventListener('dragover', this.onDragOver); - document.addEventListener('dragleave', this.onDragLeave); - document.addEventListener('drop', this.onDrop); - }); - this.on('unmount', () => { - document.removeEventListener('dragover', this.onDragOver); - document.removeEventListener('dragleave', this.onDragLeave); - document.removeEventListener('drop', this.onDrop); +textures-panel.panel.view + .flexfix.tall + div + asset-viewer( + collection="{global.currentProject.textures}" + contextmenu="{showTexturePopup}" + vocspace="texture" + namespace="textures" + click="{openTexture}" + thumbnails="{thumbnails}" + ref="textures" + ) + label.file.flexfix-header + input(type="file" multiple + accept=".png,.jpg,.jpeg,.bmp,.gif,.json" + onchange="{parent.textureImport}") + .button + svg.feather + use(xlink:href="data/icons.svg#download") + span {voc.import} + asset-viewer( + collection="{global.currentProject.skeletons}" + contextmenu="{showSkeletonPopup}" + vocspace="texture" + namespace="skeletons" + thumbnails="{thumbnails}" + ref="skeletons" + ) + h2 + span {voc.skeletons} + docs-shortcut(path="/skeletal-animation.html") + label.file.flexfix-header + input(type="file" multiple + accept=".json" + onchange="{parent.textureImport}") + .button + svg.feather + use(xlink:href="data/icons.svg#download") + span {voc.import} + + .aDropzone(if="{dropping}") + .middleinner + svg.feather + use(xlink:href="data/icons.svg#download") + h2 {languageJSON.common.fastimport} + input(type="file" multiple + accept=".png,.jpg,.jpeg,.bmp,.gif,.json" + onchange="{textureImport}") + + texture-editor(if="{editing}" texture="{currentTexture}") + context-menu(menu="{textureMenu}" ref="textureMenu") + script. + const fs = require('fs-extra'), + path = require('path'); + const glob = require('./data/node_requires/glob'); + const generateGUID = require('./data/node_requires/generateGUID'); + this.namespace = 'texture'; + this.mixin(window.riotVoc); + this.editing = false; + this.dropping = false; + + const {getTexturePreview} = require('./data/node_requires/resources/textures'); + // this.thumbnails = texture => `file://${global.projdir}/img/${texture.origname}_prev.png?cache=${texture.lastmod}`; + this.thumbnails = getTexturePreview; + + this.fillTextureMap = () => { + glob.texturemap = {}; + global.currentProject.textures.forEach(texture => { + var img = document.createElement('img'); + glob.texturemap[texture.uid] = img; + img.g = texture; + img.src = 'file://' + global.projdir + '/img/' + texture.origname + '?' + texture.lastmod; + }); + var img = document.createElement('img'); + glob.texturemap[-1] = img; + img.g = { + width: 32, + height: 32, + offx: 0, + offy: 0, + grid: [1, 1], + axis: [16, 16], + marginx: 0, + marginy: 0, + imgWidth: 32, + imgHeight: 32, + closedStrip: true + }; + img.src = 'data/img/unknown.png'; + }; + + this.setUpPanel = () => { + this.fillTextureMap(); + this.refs.textures.updateList(); + this.refs.skeletons.updateList(); + this.searchResults = null; + this.editing = false; + this.dropping = false; + this.currentTexture = null; + this.update(); + }; + this.updateTextureData = () => { + this.refs.textures.updateList(); + this.refs.skeletons.updateList(); + this.update(); + this.fillTextureMap(); + }; + + window.signals.on('projectLoaded', this.setUpPanel); + window.signals.on('textureImported', this.updateTextureData); + this.on('mount', this.setUpPanel); + this.on('unmount', () => { + window.signals.off('projectLoaded', this.setUpPanel); + window.signals.off('textureImported', this.updateTextureData); + }); + + /** + * An event fired when user attempts to add files from a file manager + * (by clicking an "Import" button) + */ + this.textureImport = e => { // input[type="file"] + const {importImageToTexture} = require('./data/node_requires/resources/textures'); + const files = [...e.target.files].map(file => file.path); + for (let i = 0; i < files.length; i++) { + if (/\.(jpg|gif|png|jpeg)/gi.test(files[i])) { + importImageToTexture(files[i]); + } else if (/_ske\.json/i.test(files[i])) { + const id = generateGUID(); + this.loadSkeleton( + id, + files[i], + global.projdir + '/img/skdb' + id + '_ske.json' + ); + } + } + e.srcElement.value = ''; + this.dropping = false; + e.preventDefault(); + }; + + this.loadSkeleton = (uid, filename, dest) => { + fs.copy(filename, dest) + .then(() => fs.copy(filename.replace('_ske.json', '_tex.json'), dest.replace('_ske.json', '_tex.json'))) + .then(() => fs.copy(filename.replace('_ske.json', '_tex.png'), dest.replace('_ske.json', '_tex.png'))) + .then(() => { + global.currentProject.skeletons.push({ + name: path.basename(filename).replace('_ske.json', ''), + origname: path.basename(dest), + from: 'dragonbones', + uid + }); + this.skelGenPreview(dest, dest + '_prev.png', [64, 128]) + .then(() => { + this.refs.skeletons.updateList(); + this.update(); + }); + }); + }; + + /** + * Generates a square preview for a given skeleton + * @param {String} source Path to the source _ske.json file + * @param {String} destFile Path to the destinating image + * @param {Array} sizes Size of the square thumbnail, in pixels + * @returns {Promise} Resolves after creating a thumbnail. On success, + * passes data-url of the created thumbnail. + */ + this.skelGenPreview = (source, destFile, sizes) => { + // TODO: Actually generate previews of different sizes + const loader = new PIXI.loaders.Loader(), + dbf = dragonBones.PixiFactory.factory; + const slice = 'file://' + source.replace('_ske.json', ''); + return new Promise((resolve, reject) => { + loader.add(`${slice}_ske.json`, `${slice}_ske.json`) + .add(`${slice}_tex.json`, `${slice}_tex.json`) + .add(`${slice}_tex.png`, `${slice}_tex.png`); + loader.load(() => { + dbf.parseDragonBonesData(loader.resources[`${slice}_ske.json`].data); + dbf.parseTextureAtlasData(loader.resources[`${slice}_tex.json`].data, loader.resources[`${slice}_tex.png`].texture); + const skel = dbf.buildArmatureDisplay('Armature', loader.resources[`${slice}_ske.json`].data.name); + const promises = sizes.map(() => new Promise((resolve, reject) => { + const app = new PIXI.Application(); + const rawSkelBase64 = app.renderer.plugins.extract.base64(skel); + const skelBase64 = rawSkelBase64.replace(/^data:image\/\w+;base64,/, ''); + const buf = new Buffer(skelBase64, 'base64'); + const stream = fs.createWriteStream(destFile); + stream.on('finish', () => { + setTimeout(() => { // WHY THE HECK I EVER NEED THIS?! + resolve(destFile); + }, 100); + }); + stream.on('error', err => { + reject(err); + }); + stream.end(buf); + })); + Promise.all(promises) + .then(() => { + // eslint-disable-next-line no-underscore-dangle + delete dbf._dragonBonesDataMap[loader.resources[`${slice}_ske.json`].data.name]; + // eslint-disable-next-line no-underscore-dangle + delete dbf._textureAtlasDataMap[loader.resources[`${slice}_ske.json`].data.name]; + }) + .then(resolve) + .catch(reject); + }); + }); + }; + + const deleteCurrentTexture = () => { + for (const type of global.currentProject.types) { + if (type.texture === this.currentTexture.uid) { + type.texture = -1; + } + } + for (const room of global.currentProject.rooms) { + if ('tiles' in room) { + for (const layer of room.tiles) { + layer.tiles = layer.tiles.filter(tile => tile.texture !== this.currentTexture.uid); + } + } + if ('backgrounds' in room) { + let i = 0; + while (i < room.backgrounds.length) { + if (room.backgrounds[i].texture === this.currentTexture.uid) { + room.backgrounds.splice(i, 1); + } else { + i++; + } + } + } + } + for (const tandem of global.currentProject.emitterTandems) { + for (const emitter of tandem.emitters) { + if (emitter.texture === this.currentTexture.uid) { + emitter.texture = -1; + } + } + } + if (global.currentProject.settings.icon === this.currentTexture.uid) { + delete global.currentProject.settings.icon; + } + global.currentProject.textures.splice(this.currentTextureId, 1); + }; + + // Creates a context menu that will appear on RMB click on texture cards + this.textureMenu = { + opened: false, + items: [{ + label: window.languageJSON.common.open, + click: () => { + if (this.currentTextureType !== 'skeleton') { + this.openTexture(this.currentTexture)(); + } + this.update(); + } + }, { + label: window.languageJSON.common.copyName, + click: () => { + nw.Clipboard.get().set(this.currentTexture.name, 'text'); + } + }, { + label: window.languageJSON.common.rename, + click: () => { + alertify + .defaultValue(this.currentTexture.name) + .prompt(window.languageJSON.common.newname) + .then(e => { + if (e.inputValue && e.inputValue !== '' && e.buttonClicked !== 'cancel') { + this.currentTexture.name = e.inputValue; + this.update(); + } + }); + } + }, { + type: 'separator' + }, { + label: window.languageJSON.common.delete, + click: () => { + alertify + .okBtn(window.languageJSON.common.delete) + .cancelBtn(window.languageJSON.common.cancel) + .confirm(window.languageJSON.common.confirmDelete.replace('{0}', this.currentTexture.name)) + .then(e => { + if (e.buttonClicked === 'ok') { + if (this.currentTextureType === 'skeleton') { + global.currentProject.skeletons.splice(this.currentTextureId, 1); + } else { + deleteCurrentTexture(); + } + this.refs.textures.updateList(); + this.refs.skeletons.updateList(); + this.update(); + alertify + .okBtn(window.languageJSON.common.ok) + .cancelBtn(window.languageJSON.common.cancel); + } + }); + } + }] + }; + /** + * Shows the context menu created above + */ + this.showTexturePopup = (texture, isSkeleton) => e => { + this.currentTextureType = isSkeleton ? 'skeleton' : 'texture'; + if (isSkeleton) { + this.currentTextureId = global.currentProject.skeletons.indexOf(texture); + } else { + this.currentTextureId = global.currentProject.textures.indexOf(texture); + } + this.currentTexture = texture; + this.refs.textureMenu.popup(e.clientX, e.clientY); + e.preventDefault(); + }; + this.showSkeletonPopup = skel => e => { + this.showTexturePopup(skel, true)(e); + }; + + /** + * Opens an editor for the given texture + */ + this.openTexture = texture => () => { + this.currentTexture = texture; + this.currentTextureId = global.currentProject.textures.indexOf(texture); + this.editing = true; + }; + + /* + * drag-n-drop handling + */ + var dragTimer; + this.onDragOver = e => { + var dt = e.dataTransfer; + if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) { + this.dropping = true; + this.update(); + window.clearTimeout(dragTimer); + } + e.preventDefault(); + e.stopPropagation(); + }; + this.onDrop = e => { + e.stopPropagation(); + }; + this.onDragLeave = e => { + dragTimer = window.setTimeout(() => { + this.dropping = false; + this.update(); + }, 25); + e.preventDefault(); + e.stopPropagation(); + }; + this.on('mount', () => { + document.addEventListener('dragover', this.onDragOver); + document.addEventListener('dragleave', this.onDragLeave); + document.addEventListener('drop', this.onDrop); + }); + this.on('unmount', () => { + document.removeEventListener('dragover', this.onDragOver); + document.removeEventListener('dragleave', this.onDragLeave); + document.removeEventListener('drop', this.onDrop); }); \ No newline at end of file diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 45a17028a..4436fecfd 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -86,12 +86,12 @@ type-editor.panel.view.flexrow this.refreshExtends = () => { this.libExtends = []; for (const lib in global.currentProject.libs) { - fs.readJSON(path.join(libsDir, lib, 'module.json'), (err, data) => { + fs.readJSON(path.join(libsDir, lib, 'module.json'), (err, moduleJson) => { if (err) { return; } - if (data.typeExtends) { - this.libExtends.push(...data.typeExtends); + if (moduleJson.typeExtends) { + this.libExtends.push(...moduleJson.typeExtends); } this.update(); }); @@ -108,19 +108,19 @@ type-editor.panel.view.flexrow const tabToEditor = tab => { tab = tab || this.tab; - if (this.tab === 'typeonstep') { + if (tab === 'typeonstep') { return this.typeonstep; - } else if (this.tab === 'typeondraw') { + } else if (tab === 'typeondraw') { return this.typeondraw; - } else if (this.tab === 'typeondestroy') { + } else if (tab === 'typeondestroy') { return this.typeondestroy; - } else if (this.tab === 'typeoncreate') { + } else if (tab === 'typeoncreate') { return this.typeoncreate; } return null; }; - this.changeTab = tab => e => { + this.changeTab = tab => () => { this.tab = tab; const editor = tabToEditor(tab); setTimeout(() => { @@ -181,16 +181,16 @@ type-editor.panel.view.flexrow }) ); - this.typeoncreate.onDidChangeModelContent(e => { + this.typeoncreate.onDidChangeModelContent(() => { this.type.oncreate = this.typeoncreate.getPureValue(); }); - this.typeonstep.onDidChangeModelContent(e => { + this.typeonstep.onDidChangeModelContent(() => { this.type.onstep = this.typeonstep.getPureValue(); }); - this.typeondraw.onDidChangeModelContent(e => { + this.typeondraw.onDidChangeModelContent(() => { this.type.ondraw = this.typeondraw.getPureValue(); }); - this.typeondestroy.onDidChangeModelContent(e => { + this.typeondestroy.onDidChangeModelContent(() => { this.type.ondestroy = this.typeondestroy.getPureValue(); }); this.typeoncreate.focus(); @@ -198,8 +198,7 @@ type-editor.panel.view.flexrow }); this.on('update', () => { if (global.currentProject.types.find(type => - this.type.name === type.name && this.type !== type - )) { + this.type.name === type.name && this.type !== type)) { this.nameTaken = true; } else { this.nameTaken = false; @@ -213,10 +212,10 @@ type-editor.panel.view.flexrow this.typeondestroy.dispose(); }); - this.changeSprite = e => { + this.changeSprite = () => { this.selectingTexture = true; }; - this.applyTexture = texture => e => { + this.applyTexture = texture => () => { if (texture === -1) { this.type.texture = -1; } else { @@ -229,11 +228,11 @@ type-editor.panel.view.flexrow this.parent.fillTypeMap(); this.update(); }; - this.cancelTexture = e => { + this.cancelTexture = () => { this.selectingTexture = false; this.update(); }; - this.typeSave = e => { + this.typeSave = () => { if (this.nameTaken) { // animate the error notice require('./data/node_requires/jellify')(this.refs.errorNotice); @@ -241,9 +240,10 @@ type-editor.panel.view.flexrow return false; } glob.modified = true; - this.type.lastmod = +(new Date()); + this.type.lastmod = Number(new Date()); this.parent.editingType = false; this.parent.fillTypeMap(); this.parent.update(); window.signals.trigger('typesChanged'); + return true; }; diff --git a/src/riotTags/types-panel.tag b/src/riotTags/types-panel.tag index ccf38acad..c1bc156a1 100644 --- a/src/riotTags/types-panel.tag +++ b/src/riotTags/types-panel.tag @@ -25,11 +25,11 @@ types-panel.panel.view this.sort = 'name'; this.sortReverse = false; - this.thumbnails = type => type.texture !== -1 ? + this.thumbnails = type => (type.texture !== -1 ? `${glob.texturemap[type.texture].src.split('?')[0]}_prev.png?cache=${this.getTypeTextureRevision(type)}` : - 'data/img/notexture.png'; + 'data/img/notexture.png'); - this.setUpPanel = e => { + this.setUpPanel = () => { this.fillTypeMap(); this.refs.types.updateList(); this.searchResults = null; @@ -77,8 +77,9 @@ types-panel.panel.view if (!e) { this.update(); } + return true; }; - this.openType = type => e => { + this.openType = type => () => { this.editingType = true; this.editedType = type; }; @@ -91,8 +92,8 @@ types-panel.panel.view this.update(); } }, { - label: languageJSON.common.copyName, - click: e => { + label: window.languageJSON.common.copyName, + click: () => { nw.Clipboard.get().set(this.currentType.name, 'text'); } }, { @@ -102,7 +103,7 @@ types-panel.panel.view .defaultValue(this.currentType.name + '_dup') .prompt(window.languageJSON.common.newname) .then(e => { - if (e.inputValue != '' && e.buttonClicked !== 'cancel') { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { var tp = JSON.parse(JSON.stringify(this.currentType)); tp.name = e.inputValue; tp.uid = generateGUID(); @@ -115,12 +116,12 @@ types-panel.panel.view } }, { label: window.languageJSON.common.rename, - click: () => { + click: () => { alertify .defaultValue(this.currentType.name) .prompt(window.languageJSON.common.newname) .then(e => { - if (e.inputValue != '' && e.buttonClicked !== 'cancel') { + if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { this.currentType.name = e.inputValue; this.update(); } @@ -148,7 +149,7 @@ types-panel.panel.view } } - let ind = global.currentProject.types.indexOf(this.currentType); + const ind = global.currentProject.types.indexOf(this.currentType); global.currentProject.types.splice(ind, 1); this.refs.types.updateList(); this.fillTypeMap(); From 381b205cf8a955c75e630adeaa8ad50d4365130f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 31 May 2020 20:59:52 +1200 Subject: [PATCH 02/86] :bug: Fix that last linter issue in texture-editor --- src/riotTags/texture-editor.tag | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/riotTags/texture-editor.tag b/src/riotTags/texture-editor.tag index 159253451..43990c8d6 100644 --- a/src/riotTags/texture-editor.tag +++ b/src/riotTags/texture-editor.tag @@ -228,8 +228,7 @@ texture-editor.panel.view var textureCanvas, grprCanvas; this.on('mount', () => { - textureCanvas = this.refs.textureCanvas; - grprCanvas = this.refs.grprCanvas; + ({textureCanvas, grprCanvas} = this.refs); textureCanvas.x = textureCanvas.getContext('2d'); grprCanvas.x = grprCanvas.getContext('2d'); var texture = this.texture = this.opts.texture; From b5ecf8a8b35f7529dacd08f3d148a295038ca93b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 5 Jun 2020 21:01:55 +1200 Subject: [PATCH 03/86] :zap: Refurbish project's settings screen :fire: Remove export options: HTML and CSS are now always minified, and JS conversion never worked correctly --- app/data/i18n/Brazilian Portuguese.json | 52 ++----- app/data/i18n/Chinese Simplified.json | 69 +++++---- app/data/i18n/Dutch.json | 79 +++++------ app/data/i18n/English.json | 94 ++++++------- app/data/i18n/French.json | 82 +++++------ app/data/i18n/German.json | 67 ++++----- app/data/i18n/Polish.json | 67 ++++----- app/data/i18n/Romanian.json | 65 ++++----- app/data/i18n/Russian.json | 89 ++++++------ app/data/i18n/Spanish.json | 65 ++++----- src/icons/sliders.svg | 12 ++ src/js/migration/1.3.2.js | 43 ++++++ src/node_requires/exporter/icons.js | 4 +- src/node_requires/exporter/index.js | 131 ++++++------------ .../resources/projects/defaultProject.js | 20 ++- src/riotTags/actions-editor.tag | 116 ---------------- src/riotTags/export-panel.tag | 46 +++--- src/riotTags/main-menu.tag | 10 +- .../actions-input-selector.tag} | 10 +- .../project-settings/actions-settings.tag | 90 ++++++++++++ .../project-settings/authoring-settings.tag | 37 +++++ .../project-settings/branding-settings.tag | 32 +++++ .../project-settings/project-settings.tag | 36 +++++ .../project-settings/rendering-settings.tag | 31 +++++ .../{ => project-settings}/script-editor.tag | 2 +- .../scripts-setttings.tag} | 8 +- .../project-settings/structure-editor.tag | 0 src/riotTags/settings-panel.tag | 95 ------------- src/riotTags/shared/texture-input.tag | 6 +- src/riotTags/shared/texture-selector.tag | 3 + src/styl/buildingBlocks.styl | 17 ++- src/styl/tags/actions-editor.styl | 5 - src/styl/tags/scripts-panel.styl | 15 -- src/styl/tags/settings-panel.styl | 5 - .../actions-input-selector.styl} | 6 +- src/styl/tags/settings/actions-settings.styl | 7 + src/styl/tags/settings/project-settings.styl | 20 +++ .../tags/{ => settings}/script-editor.styl | 0 src/styl/tags/settings/scripts-panel.styl | 22 +++ 39 files changed, 774 insertions(+), 784 deletions(-) create mode 100644 src/icons/sliders.svg create mode 100644 src/js/migration/1.3.2.js delete mode 100644 src/riotTags/actions-editor.tag rename src/riotTags/{method-selector.tag => project-settings/actions-input-selector.tag} (93%) create mode 100644 src/riotTags/project-settings/actions-settings.tag create mode 100644 src/riotTags/project-settings/authoring-settings.tag create mode 100644 src/riotTags/project-settings/branding-settings.tag create mode 100644 src/riotTags/project-settings/project-settings.tag create mode 100644 src/riotTags/project-settings/rendering-settings.tag rename src/riotTags/{ => project-settings}/script-editor.tag (99%) rename src/riotTags/{scripts-panel.tag => project-settings/scripts-setttings.tag} (96%) create mode 100644 src/riotTags/project-settings/structure-editor.tag delete mode 100644 src/riotTags/settings-panel.tag delete mode 100644 src/styl/tags/actions-editor.styl delete mode 100644 src/styl/tags/scripts-panel.styl delete mode 100644 src/styl/tags/settings-panel.styl rename src/styl/tags/{method-selector.styl => settings/actions-input-selector.styl} (70%) create mode 100644 src/styl/tags/settings/actions-settings.styl create mode 100644 src/styl/tags/settings/project-settings.styl rename src/styl/tags/{ => settings}/script-editor.styl (100%) create mode 100644 src/styl/tags/settings/scripts-panel.styl diff --git a/app/data/i18n/Brazilian Portuguese.json b/app/data/i18n/Brazilian Portuguese.json index 0aa74e545..100555f3a 100644 --- a/app/data/i18n/Brazilian Portuguese.json +++ b/app/data/i18n/Brazilian Portuguese.json @@ -126,33 +126,27 @@ "codeDense": "" }, "settings": { - "author": "Nome do autor:", - "authoring": "Autoria", - "cover": "Capa", - "exportparams": "Exportar parâmetros", - "framerate": "Taxa de quadros", - "getfile": "Escolher", - "minifyhtmlcss": "Compactar HTML e CSS", - "minifyjs": "Compactar Javascript e converter para ES5 (lento; usar para releases)", - "pixelatedrender": "Desabilitar suavização de imagem aqui e no projeto exportado (preserva pixels nítidos)", - "renderoptions": "Opções de Renderização", - "settings": "Configurações do Projeto", - "site": "Página inicial", - "title": "Nome:", - "version": "Versão:", - "versionpostfix": "Postfix:", + "authoring": { + "author": "Nome do autor:", + "heading": "Autoria", + "site": "Página inicial", + "title": "Nome:", + "version": "Versão:", + "versionpostfix": "Postfix:" + }, + "rendering": { + "heading": "Opções de Renderização", + "pixelatedrender": "Desabilitar suavização de imagem aqui e no projeto exportado (preserva pixels nítidos)", + "framerate": "Taxa de quadros" + }, "scripts": { + "heading": "Scripts", "addNew": "Adicionar um Novo Script", "deleteScript": "Apagar o script", - "header": "Scripts", "newScriptComment": "Usar scripts para definir funções frequentes e importar pequenas bibliotecas", "moveUp": "Mova-se para cima", "moveDown": "Mova-se para baixo" - }, - "actions": "", - "editActions": "", - "highDensity": "", - "maxFPS": "" + } }, "modules": { "author": "Autor deste catmod", @@ -340,23 +334,7 @@ "helppages": "Ajuda", "backToHome": "Voltar para a página inicial da documentação" }, - "actionsEditor": { - "actions": "", - "actionsEditor": "", - "addAction": "", - "addMethod": "", - "deleteAction": "", - "deleteMethod": "", - "inputActionNamePlaceholder": "", - "methodModuleMissing": "", - "methods": "", - "multiplier": "", - "noActionsYet": "" - }, "docsShortcut": { "openDocs": "" - }, - "inputMethodSelector": { - "select": "" } } \ No newline at end of file diff --git a/app/data/i18n/Chinese Simplified.json b/app/data/i18n/Chinese Simplified.json index 8192e5ade..e48a6dff2 100644 --- a/app/data/i18n/Chinese Simplified.json +++ b/app/data/i18n/Chinese Simplified.json @@ -50,19 +50,6 @@ "zoomIn": "放大缩小字体功能 放大缩小字体功能", "zoomOut": "放大缩小字体功能 放大缩小字体功能" }, - "actionsEditor": { - "actions": "动作", - "actionsEditor": "动作编辑器", - "addAction": "添加动作", - "addMethod": "添加输入方法", - "deleteAction": "删除这个动作", - "deleteMethod": "删除这个方法", - "inputActionNamePlaceholder": "动作名称", - "methodModuleMissing": "缺少此方法所需的模块", - "methods": "输入方法", - "multiplier": "乘数", - "noActionsYet": "动作允许开发人员一次监听多个输入方法并动态地更改它们, 所有这些操作都使用一个统一的API. 点击上面的文档图标来阅读更多内容" - }, "colorPicker": { "current": "新建", "globalPalette": "全局调色盘", @@ -81,9 +68,6 @@ "firstrunnotice": "每个平台的第一次运行会比较慢, 因为ct.js会下载并保存打包所需的额外库. 这需要一些时间, 但下一次几乎是瞬间的.", "log": "消息日志" }, - "inputMethodSelector": { - "select": "选择" - }, "intro": { "loading": "请稍等: 小猫正在聚集光速!", "newProject": { @@ -151,34 +135,43 @@ "openIncludeFolder": "打开\"include\"文件夹" }, "settings": { - "actions": "动作和输入方法", - "author": "作者姓名", - "authoring": "创作", - "cover": "封面:", - "editActions": "编辑动作", - "exportparams": "导出参数", - "framerate": "帧率:", - "getfile": "选择", - "highDensity": "支持高像素密度(例如在视网膜屏幕上)", - "maxFPS": "最大帧率:", - "minifyhtmlcss": "压缩HTML和CSS", - "minifyjs": "压缩JavaScript并将其转换为ES5 (缓慢; 用于发行版)", - "pixelatedrender": "在此处和导出的项目中禁用图像平滑 (保留清晰像素)", - "renderoptions": "渲染选项", - "settings": "项目设置", - "site": "主页:", - "title": "名称:", - "version": "版本:", - "versionpostfix": "后缀:", + "actions": { + "heading": "动作和输入方法", + "actions": "动作", + "addAction": "添加动作", + "addMethod": "添加输入方法", + "deleteAction": "删除这个动作", + "deleteMethod": "删除这个方法", + "inputActionNamePlaceholder": "动作名称", + "methodModuleMissing": "缺少此方法所需的模块", + "methods": "输入方法", + "multiplier": "乘数", + "noActionsYet": "动作允许开发人员一次监听多个输入方法并动态地更改它们, 所有这些操作都使用一个统一的API. 点击上面的文档图标来阅读更多内容" + }, + "authoring": { + "author": "作者姓名", + "heading": "创作", + "site": "主页:", + "title": "名称:", + "version": "版本:", + "versionpostfix": "后缀:" + }, + "rendering": { + "heading": "渲染选项", + "framerate": "帧率:", + "usePixiLegacy": "添加基于画布的旧版渲染器以支持较旧的浏览器和图形卡 (在您的游戏中最多增加20kb)", + "highDensity": "支持高像素密度(例如在视网膜屏幕上)", + "maxFPS": "最大帧率:", + "pixelatedrender": "在此处和导出的项目中禁用图像平滑 (保留清晰像素)" + }, "scripts": { + "heading": "脚本", "addNew": "添加新脚本", "deleteScript": "删除脚本", - "header": "脚本", "newScriptComment": "使用脚本定义常用函数并导入小型库", "moveUp": "向上移动", "moveDown": "向下移动" - }, - "usePixiLegacy": "添加基于画布的旧版渲染器以支持较旧的浏览器和图形卡 (在您的游戏中最多增加20kb)" + } }, "modules": { "author": "这个cat模组的作者", diff --git a/app/data/i18n/Dutch.json b/app/data/i18n/Dutch.json index 0275bab37..4e975b910 100644 --- a/app/data/i18n/Dutch.json +++ b/app/data/i18n/Dutch.json @@ -51,19 +51,6 @@ "zoomIn": "Zoom in", "zoomOut": "Zoom uit" }, - "actionsEditor": { - "actions": "Acties", - "actionsEditor": "Acties editor", - "addAction": "Toevoegen van een actie", - "addMethod": "Toevoegen van een input methode", - "deleteAction": "Verwijder deze actie", - "deleteMethod": "Verwijder deze methode", - "inputActionNamePlaceholder": "Actie naam", - "methodModuleMissing": "The benodigde module voor deze methode mist", - "methods": "Invoermethoden", - "multiplier": "Vermenigvuldiger", - "noActionsYet": "Met acties kunnen ontwikkelaars naar verschillende invoermethoden tegelijk luisteren en deze dynamisch wijzigen, allemaal met een uniforme API. Lees meer door op het documentatie-pictogram hierboven te klikken." - }, "colorPicker": { "current": "New", "globalPalette": "Globaal Palet", @@ -102,17 +89,8 @@ "firstrunnotice": "De eerste run voor elk platform zal traag zijn, omdat ct.js extra bibliotheken zal downloaden en degene opslaan die nodig zijn voor het inpakken. Het zal even duren, maar de volgende keer zal bijna onmiddellijk zijn.", "log": "Berichtenlog", "windowsCrossBuildWarning": "Om vanaf Linux / MacOS voor Windows te bouwen, moet Wine op uw systeem zijn geïnstalleerd. Installatie-instructies zijn verschillend voor verschillende platforms, dus u kunt het beter zelf googlen :)", - "launchMode": "Lanceer modes:", - "launchModes": { - "maximized": "Maximaliseren", - "fullscreen": "Volledig scherm", - "windowed": "Windowed" - }, "cannotBuildForMacOnWin": "Helaas kan Windows alleen kapotte Mac-pakketten produceren. Probeer een Linux-machine te gebruiken; bijvoorbeeld als een virtuele machine. Dit kan 100% gratis!" }, - "inputMethodSelector": { - "select": "Selecteer" - }, "intro": { "loading": "Even geduld: kittens verzamelen lichtsnelheid!", "newProject": { @@ -198,26 +176,41 @@ "showOnboardingCheckbox": "Laat dit scherm zien wanneer er een nieuw project wordt gecreëerd" }, "settings": { - "actions": "Acties en Invoermethoden", - "author": "Auteru naam:", - "authoring": "Auteur", - "cover": "Dek:", - "editActions": "Acties aanpassen", - "exportparams": "Exporteer parameters", - "framerate": "Framerate:", - "getfile": "Kiezen", - "highDensity": "Ondersteuning voor hoge pixeldichtheid (bijv. Op retina-schermen)", - "maxFPS": "Maximale framerate:", - "minifyhtmlcss": "Comprimeer HTML en CSS", - "minifyjs": "Comprimeer JavaScript en converteer het naar ES5 (langzaam; gebruik voor releases)", - "usePixiLegacy": "Voeg een verouderde, canvasgebaseerde renderer toe om oudere browsers en grafische kaarten te ondersteunen (voegt ~20kb toe aan je spel)", - "pixelatedrender": "Schakel het vloeiend maken van afbeeldingen hier en in een geëxporteerd project uit (behoud scherpe pixels)", - "renderoptions": "Render Opties", - "settings": "Project instellingen", - "site": "Thuispagina:", - "title": "Naam:", - "version": "Versie:", - "versionpostfix": "Postfix:", + "actions": { + "heading": "Acties en Invoermethoden", + "actions": "Acties", + "addAction": "Toevoegen van een actie", + "addMethod": "Toevoegen van een input methode", + "deleteAction": "Verwijder deze actie", + "deleteMethod": "Verwijder deze methode", + "inputActionNamePlaceholder": "Actie naam", + "methodModuleMissing": "The benodigde module voor deze methode mist", + "methods": "Invoermethoden", + "multiplier": "Vermenigvuldiger", + "noActionsYet": "Met acties kunnen ontwikkelaars naar verschillende invoermethoden tegelijk luisteren en deze dynamisch wijzigen, allemaal met een uniforme API. Lees meer door op het documentatie-pictogram hierboven te klikken." + }, + "authoring": { + "heading": "Auteur", + "author": "Auteru naam:", + "site": "Thuispagina:", + "title": "Naam:", + "version": "Versie:", + "versionpostfix": "Postfix:" + }, + "rendering": { + "heading": "Render Opties", + "framerate": "Framerate:", + "highDensity": "Ondersteuning voor hoge pixeldichtheid (bijv. Op retina-schermen)", + "maxFPS": "Maximale framerate:", + "usePixiLegacy": "Voeg een verouderde, canvasgebaseerde renderer toe om oudere browsers en grafische kaarten te ondersteunen (voegt ~20kb toe aan je spel)", + "pixelatedrender": "Schakel het vloeiend maken van afbeeldingen hier en in een geëxporteerd project uit (behoud scherpe pixels)", + "launchMode": "Lanceer modes:", + "launchModes": { + "maximized": "Maximaliseren", + "fullscreen": "Volledig scherm", + "windowed": "Windowed" + } + }, "branding": { "heading": "Branding", "icon": "Game's icoon:", @@ -229,7 +222,7 @@ "scripts": { "addNew": "Toevoegen van een nieuw Script", "deleteScript": "Verwijder een Script", - "header": "Scripts", + "heading": "Scripts", "newScriptComment": "Gebruik scripts om veelgebruikte functies te definiëren en kleine bibliotheken te importeren" } }, diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index d716cab38..4b91cba36 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -25,7 +25,7 @@ "fastimport": "Fast Import", "language": "Language", "loading": "Loading…", - "translateToYourLanguage": "Translate ct.js to your language!", + "translateToYourLanguage": "Translate ct.js!", "name": "Name:", "nametaken": "This name is already taken", "newname": "New name:", @@ -42,7 +42,8 @@ "save": "Save", "savedcomm": "Your project was succesfully saved.", "saveproject": "Save project", - "select": "Select…", + "selectDialogue": "Select…", + "select": "Select", "sort": "Sort:", "tilelayer": "tile layer", "wrongFormat": "Wrong file format", @@ -51,19 +52,6 @@ "zoomIn": "Zoom in", "zoomOut": "Zoom out" }, - "actionsEditor": { - "actions": "Actions", - "actionsEditor": "Actions editor", - "addAction": "Add an action", - "addMethod": "Add an input method", - "deleteAction": "Delete this action", - "deleteMethod": "Delete this method", - "inputActionNamePlaceholder": "Action name", - "methodModuleMissing": "The required module for this method is missing", - "methods": "Input methods", - "multiplier": "Multiplier", - "noActionsYet": "Actions allow developers to listen to numerous input methods at once and dynamically change them, all with one uniform API. Read more by clicking on the docs icon above." - }, "colorPicker": { "current": "New", "globalPalette": "Global Palette", @@ -102,17 +90,8 @@ "firstrunnotice": "The first run for each platform will be slow as ct.js will download and save additional libraries needed for packing. It will take some time, but next times will be almost instant.", "log": "Message Log", "windowsCrossBuildWarning": "To build for Windows from Linux/MacOS, you need Wine installed in your system. Install instructions are all different for various platforms, so you better google it yourself :)", - "launchMode": "Launch mode:", - "launchModes": { - "maximized": "Maximized", - "fullscreen": "Fullscreen", - "windowed": "Windowed" - }, "cannotBuildForMacOnWin": "Unfortunately, Windows can only produce broken Mac packages. Try using a Linux machine; for example, in a virtual one. It is 100% free!" }, - "inputMethodSelector": { - "select": "Select" - }, "intro": { "loading": "Please wait: kittens are gathering speed of light!", "newProject": { @@ -158,6 +137,7 @@ "rooms": "Rooms", "save": "Save project", "startScreen": "Return to the starting screen", + "project": "Project", "settings": "Settings", "sounds": "Sounds", "successZipExport": "Successfully exported to {0}.", @@ -199,41 +179,57 @@ "showOnboardingCheckbox": "Show this screen when creating a new project" }, "settings": { - "actions": "Actions and input methods", - "author": "Author name:", - "authoring": "Authoring", - "cover": "Cover:", - "editActions": "Edit actions", - "exportparams": "Export parameters", - "framerate": "Framerate:", - "getfile": "Choose", - "highDensity": "Support high pixel density (e.g. on retina screens)", - "maxFPS": "Max framerate:", - "minifyhtmlcss": "Compress HTML and CSS", - "minifyjs": "Compress JavaScript and convert it to ES5 (slow; use for releases)", - "usePixiLegacy": "Add a legacy, canvas-based renderer to support older browsers and graphics cards (adds ~20kb up to your game)", - "pixelatedrender": "Disable image smoothing here and in exported project (preserve crisp pixels)", - "renderoptions": "Render Options", - "settings": "Project settings", - "site": "Homepage:", - "title": "Name:", - "version": "Version:", - "versionpostfix": "Postfix:", + "actions": { + "heading": "Actions and input methods", + "actions": "Actions", + "addAction": "Add an action", + "addMethod": "Add an input method", + "deleteAction": "Delete this action", + "deleteMethod": "Delete this method", + "inputActionNamePlaceholder": "Action name", + "methodModuleMissing": "The required module for this method is missing", + "methods": "Input methods", + "multiplier": "Multiplier", + "noActionsYet": "Actions allow developers to listen to numerous input methods at once and dynamically change them, all with one uniform API. Read more by clicking on the docs icon above." + }, + "authoring": { + "heading": "Authoring", + "author": "Author name:", + "site": "Homepage:", + "title": "Name:", + "version": "Version:", + "versionpostfix": "Postfix:" + }, "branding": { "heading": "Branding", - "icon": "Game's icon:", - "iconNotice": "It should be a square, one-frame texture that is at least 256x256px large.", "accent": "Accent:", "accentNotice": "Sets the color of the preloader, as well as of some other places if used as a mobile app.", + "icon": "Game's icon:", + "iconNotice": "It should be a square, one-frame texture that is at least 256x256px large.", "invertPreloaderScheme": "Invert preloader's color scheme" }, + "rendering": { + "heading": "Render Options", + "framerate": "Framerate:", + "highDensity": "Support high pixel density (e.g. on retina screens)", + "maxFPS": "Max framerate:", + "pixelatedrender": "Disable image smoothing here and in exported project (preserve crisp pixels)", + "usePixiLegacy": "Add a legacy, canvas-based renderer to support older browsers and graphics cards (adds ~20kb up to your game)", + "desktopBuilds": "Desktop builds", + "launchMode": "Launch mode:", + "launchModes": { + "maximized": "Maximized", + "fullscreen": "Fullscreen", + "windowed": "Windowed" + } + }, "scripts": { + "heading": "Custom scripts", "addNew": "Add a New Script", "deleteScript": "Delete the script", - "header": "Scripts", - "newScriptComment": "Use scripts to define frequent functions and import small libraries", + "moveDown": "Move down", "moveUp": "Move up", - "moveDown": "Move down" + "newScriptComment": "Use scripts to define frequent functions and import small libraries" } }, "modules": { diff --git a/app/data/i18n/French.json b/app/data/i18n/French.json index 57303d3cf..d4f28133d 100644 --- a/app/data/i18n/French.json +++ b/app/data/i18n/French.json @@ -85,13 +85,7 @@ "exportPanel": "Exporter le Projet", "firstrunnotice": "Le premier lancement pour chaque plateforme sera lent car ct.js va télécharger et sauvegarder les librairies additionnels requises pour les rassembler. Cela va prendre un certain temps, mais les prochaines fois ce sera presque instantané.", "log": "Journal des logs", - "windowsCrossBuildWarning": "Pour construire pour windows depuis Linux/MacOS, vous avez besoin de Wine :)", - "launchMode": "Mode de lancement:", - "launchModes": { - "maximized": "Maximiser", - "fullscreen": "Plein écran", - "windowed": "Fenêtré" - } + "windowsCrossBuildWarning": "Pour construire pour windows depuis Linux/MacOS, vous avez besoin de Wine :)" }, "intro": { "loading": "Veuillez patientez: des chatons ", @@ -169,26 +163,41 @@ "showOnboardingCheckbox": "Afficher cet écran lors de la création d'un nouveau projet" }, "settings": { - "author": "Nom de l'auteur:", - "authoring": "Auteur", - "cover": "Couverture:", - "exportparams": "Paramétres d'exportation", - "framerate": "Taux d'image:", - "getfile": "Choisir", - "maxFPS": "Max framerate:", - "minifyhtmlcss": "Compresse le HTML et CSS", - "minifyjs": "Compresse le JavaScript et converti en ES5 (lent; utilisé pour la publication)", - "usePixiLegacy": "Ajoute un rendu hérité basé sur canvas pour prendre en charge les anciens navigateurs et cartes graphiques (ajoute environ 20 Ko à votre jeu)", - "pixelatedrender": "Désactive le lissage d'image ici et dans le projet exporté (conservez les pixels net)", - "renderoptions": "Options de rendu", - "settings": "Paramètres du projet", - "site": "Page d'accueil du projet:", - "title": "Nom du projet:", - "version": "Version:", - "versionpostfix": "Postfix:", - "actions": "Réglages des touches", - "editActions": "Configurer des touches", - "highDensity": "Prend en charge une densité de pixels élevée (par exemple sur les écrans rétina)", + "actions": { + "heading": "Réglages des touches", + "actions": "Actions", + "addAction": "Ajouter une action", + "addMethod": "Ajouter une méthode d'entrée", + "deleteAction": "Supprimer une action", + "deleteMethod": "Supprimer une méthode", + "inputActionNamePlaceholder": "Nom de l'action", + "methodModuleMissing": "Le module pour cette méthode est introuvable", + "methods": "Méthode d'entrée", + "multiplier": "Multiplier", + "noActionsYet": "Les actions permettent aux développeurs d'écouter de nombreuses méthodes d'entrée à la fois et de les modifier dynamiquement, le tout avec une API uniforme. En savoir plus en cliquant sur l'icône des documents ci-dessus" + }, + "authoring": { + "heading": "Auteur", + "author": "Nom de l'auteur:", + "site": "Page d'accueil du projet:", + "title": "Nom du projet:", + "version": "Version:", + "versionpostfix": "Postfix:" + }, + "rendering": { + "heading": "Options de rendu", + "highDensity": "Prend en charge une densité de pixels élevée (par exemple sur les écrans rétina)", + "framerate": "Taux d'image:", + "maxFPS": "Max framerate:", + "usePixiLegacy": "Ajoute un rendu hérité basé sur canvas pour prendre en charge les anciens navigateurs et cartes graphiques (ajoute environ 20 Ko à votre jeu)", + "pixelatedrender": "Désactive le lissage d'image ici et dans le projet exporté (conservez les pixels net)", + "launchMode": "Mode de lancement:", + "launchModes": { + "maximized": "Maximiser", + "fullscreen": "Plein écran", + "windowed": "Fenêtré" + } + }, "branding": { "heading": "Étiqueter", "icon": "Logo du jeu", @@ -198,14 +207,13 @@ "invertPreloaderScheme":"Inverser le schéma de couleurs du chargement" }, "scripts": { + "heading": "Vos Scripts", "addNew": "Ajouter un nouveau Script", "deleteScript": "Supprimer le Script", - "header": "Vos Scripts", "newScriptComment": "Utilisez des scripts pour définir des fonctions fréquentes et importer de petites bibliothèques", "moveUp": "Montez", "moveDown": "Déplacez-vous vers le bas" } - }, "modules": { "author": "Auteur de ce CatMod", @@ -471,25 +479,9 @@ "helppages": "Aides", "backToHome": "Retourner à la documentations" }, - "actionsEditor": { - "actions": "Actions", - "actionsEditor": "Editeur d'actions", - "addAction": "Ajouter une action", - "addMethod": "Ajouter une méthode d'entrée", - "deleteAction": "Supprimer une action", - "deleteMethod": "Supprimer une méthode", - "inputActionNamePlaceholder": "Nom de l'action", - "methodModuleMissing": "Le module pour cette méthode est introuvable", - "methods": "Méthode d'entrée", - "multiplier": "Multiplier", - "noActionsYet": "Les actions permettent aux développeurs d'écouter de nombreuses méthodes d'entrée à la fois et de les modifier dynamiquement, le tout avec une API uniforme. En savoir plus en cliquant sur l'icône des documents ci-dessus" - }, "docsShortcut": { "openDocs": "Ouvrir la documentation" }, - "inputMethodSelector": { - "select": "Sélect" - }, "patreon": { "aboutPatrons": "Les Patrons sont des personnes qui manifestent leur soutien à CamigoGames, sous forme de dons récurrents, tout les donnateurs ne viennent pas forcément de Ct.js certain(e)s utilisent d'autres support créer par ComigoGames. Astuces si vous faites un don à ComigoGames via Patreon, vous obtiendrez un lien vers votre projet ici - c'est ma petite aide pour vos créations :)", "patronsHeader": "Nos Patrons", diff --git a/app/data/i18n/German.json b/app/data/i18n/German.json index 21677c989..f0d7d28f2 100755 --- a/app/data/i18n/German.json +++ b/app/data/i18n/German.json @@ -51,19 +51,6 @@ "zoomIn": "Zoomen in", "zoomOut": "Verkleinern" }, - "actionsEditor": { - "actions": "Actions", - "actionsEditor": "Actions Editor", - "addAction": "Eine Action hinzufügen", - "addMethod": "Eine Eingabemethode hinzufügen", - "deleteAction": "Diese Action löschen", - "deleteMethod": "Diese Method löschen", - "inputActionNamePlaceholder": "Action Name", - "methodModuleMissing": "Das für diese Methode benötigte Modul existiert nicht", - "methods": "Eingabemethoden", - "multiplier": "Multiplier", - "noActionsYet": "Actions erlauben Entwicklern auf zahlreiche Eingabemethoden gleichzeitig zu zuzugreifen und sie dynamisch zu verändern. Alles mit der gleichen API. Klicken Sie oben auf das Doks-Icon um mehr darüber zu erfahren." - }, "colorPicker": { "current": "Neu", "globalPalette": "Globale Palette", @@ -102,9 +89,6 @@ "firstrunnotice": "Die erste Ausführung wird pro Plattform etwas länger dauern, da ct.js zusätzliche Bibliotheken zum Paketieren herunterladen muss. Dies wird einige Zeit dauern, beim nächsten Mal geht es deutlich schneller.", "log": "Nachrichten Log" }, - "inputMethodSelector": { - "select": "Auswählen" - }, "intro": { "loading": "Bitte warten: Kätzchen werden auf Lichtgeschwindigkeit beschleunigt!", "newProject": { @@ -181,26 +165,35 @@ "showOnboardingCheckbox": "Diesen Bildschirm anzeigen wenn ein neues Projekt erstellt wird" }, "settings": { - "actions": "Actions und Eingabemethoden", - "author": "Name des Autors:", - "authoring": "Autor", - "cover": "Cover:", - "editActions": "Actions bearbeiten", - "exportparams": "Export Parameter", - "framerate": "Framerate:", - "getfile": "Auswählen", - "highDensity": "Hochauflösende Bildschirme unterstützen (z.B. Retina Displays)", - "maxFPS": "Max Framerate:", - "minifyhtmlcss": "HTML and CSS komprimieren", - "minifyjs": "JavaScript komprimieren und in ES5 konvertieren (langsam; Sinnvoll für das Release)", - "usePixiLegacy": "Füge einen Canvas basierten Renderer hinzu, um ältere Browser und Grafikkarten zu unterstützen (Spiele wachsen um ca. 20kb)", - "pixelatedrender": "Image Smoothing hier und im exportierten Projekt deaktivieren (scharfe Pixel beibehalten)", - "renderoptions": "Render Optionen", - "settings": "Project Einstellungen", - "site": "Homepage:", - "title": "Name:", - "version": "Version:", - "versionpostfix": "Postfix:", + "actions": { + "heading": "Actions und Eingabemethoden", + "actions": "Actions", + "addAction": "Eine Action hinzufügen", + "addMethod": "Eine Eingabemethode hinzufügen", + "deleteAction": "Diese Action löschen", + "deleteMethod": "Diese Method löschen", + "inputActionNamePlaceholder": "Action Name", + "methodModuleMissing": "Das für diese Methode benötigte Modul existiert nicht", + "methods": "Eingabemethoden", + "multiplier": "Multiplier", + "noActionsYet": "Actions erlauben Entwicklern auf zahlreiche Eingabemethoden gleichzeitig zu zuzugreifen und sie dynamisch zu verändern. Alles mit der gleichen API. Klicken Sie oben auf das Doks-Icon um mehr darüber zu erfahren." + }, + "authoring": { + "heading": "Autor", + "author": "Name des Autors:", + "site": "Homepage:", + "title": "Name:", + "version": "Version:", + "versionpostfix": "Postfix:" + }, + "rendering": { + "heading": "Render Optionen", + "framerate": "Framerate:", + "highDensity": "Hochauflösende Bildschirme unterstützen (z.B. Retina Displays)", + "maxFPS": "Max Framerate:", + "usePixiLegacy": "Füge einen Canvas basierten Renderer hinzu, um ältere Browser und Grafikkarten zu unterstützen (Spiele wachsen um ca. 20kb)", + "pixelatedrender": "Image Smoothing hier und im exportierten Projekt deaktivieren (scharfe Pixel beibehalten)" + }, "branding": { "heading": "Branding", "icon": "Icon:", @@ -210,9 +203,9 @@ "invertPreloaderScheme": "Farbschema des Preloaders invertieren" }, "scripts": { + "heading": "Skripte", "addNew": "Neues Skript hinzufügen", "deleteScript": "Skript löschen", - "header": "Skripte", "newScriptComment": "Nutzen Sie Skripte um häufige Funktionen zu definieren und Bibliotheken zu importieren", "moveUp": "Bewegen Sie sich nach oben", "moveDown": "Bewegen Sie sich nach unten" diff --git a/app/data/i18n/Polish.json b/app/data/i18n/Polish.json index cd51bad79..ccda050b2 100644 --- a/app/data/i18n/Polish.json +++ b/app/data/i18n/Polish.json @@ -50,19 +50,6 @@ "zoomIn": "Powiększanie", "zoomOut": "Pomniejszanie" }, - "actionsEditor": { - "actions": "Akcje", - "actionsEditor": "Edytor akcji", - "addAction": "Dodaj akcję", - "addMethod": "Dodaj metodę wejścia", - "deleteAction": "Usuń tę akcję", - "deleteMethod": "Usuń tę metodę", - "inputActionNamePlaceholder": "Nazwa akcji", - "methodModuleMissing": "Brak modułu potrzebnego tej metodzie", - "methods": "Metody wejścia", - "multiplier": "Mnożnik", - "noActionsYet": "Akcje pozwalają deweloperom na nasłuchiwanie licznych metod wejścia jednocześnie i na dynamiczną ich zmianę, wszystko za pomocą jednolitego API. Przeczytaj więcej klikając ikonę dokumentu na górze." - }, "colorPicker": { "current": "Nowy", "globalPalette": "Paleta Globalna", @@ -81,9 +68,6 @@ "firstrunnotice": "Pierwsze uruchomienie dla każdej platformy będzie wolne, ponieważ ct.js będzie pobierać i zapisywać dodatkowe biblioteki wymagane do spakowania gry. Może to zająć trochę czasu, ale następne razy będą nimal natychmiastowe.", "log": "Log Wiadomości" }, - "inputMethodSelector": { - "select": "Wybierz" - }, "intro": { "loading": "Please wait: kittens are gathering speed of light!", "newProject": { @@ -159,30 +143,39 @@ "showOnboardingCheckbox": "Pokaż ten ekran przy tworzeniu nowego projektu" }, "settings": { - "actions": "Akcje i metody wejścia", - "author": "Autor:", - "authoring": "Authoring", - "cover": "Okładka:", - "editActions": "Edytuj akcje", - "exportparams": "Eksportuj parametry", - "framerate": "Klatki na sekundę:", - "getfile": "Wybierz", - "highDensity": "Wspieraj dużą gęstość pikseli (np. na wyświetlaczach retina)", - "maxFPS": "Maksymalna ilość klatek na sekundę:", - "minifyhtmlcss": "Kompresuj HTML i CSS", - "minifyjs": "Kompresuj JavaScript i konwertuj go do ES5 (wolne; używać do publikowania)", - "usePixiLegacy": "Dodaj szeroko wspierany, bazujący na canvasie renderer żeby wspierać stare przeglądarki i karty graficzne (dodaje ~20kb do twojej gry)", - "pixelatedrender": "Zablokuj wygładzanie obrazu tu i w eksportowanym projekcie (zachowaj ostre piksele)", - "renderoptions": "Opcje renderowania", - "settings": "Ustawienia projektu", - "site": "Strona domowa:", - "title": "Nazwa:", - "version": "Wersja:", - "versionpostfix": "Postfiks:", + "actions": { + "heading": "Akcje i metody wejścia", + "actions": "Akcje", + "addAction": "Dodaj akcję", + "addMethod": "Dodaj metodę wejścia", + "deleteAction": "Usuń tę akcję", + "deleteMethod": "Usuń tę metodę", + "inputActionNamePlaceholder": "Nazwa akcji", + "methodModuleMissing": "Brak modułu potrzebnego tej metodzie", + "methods": "Metody wejścia", + "multiplier": "Mnożnik", + "noActionsYet": "Akcje pozwalają deweloperom na nasłuchiwanie licznych metod wejścia jednocześnie i na dynamiczną ich zmianę, wszystko za pomocą jednolitego API. Przeczytaj więcej klikając ikonę dokumentu na górze." + }, + "authoring": { + "heading": "Authoring", + "author": "Autor:", + "site": "Strona domowa:", + "title": "Nazwa:", + "version": "Wersja:", + "versionpostfix": "Postfiks:" + }, + "rendering": { + "heading": "Opcje renderowania", + "framerate": "Klatki na sekundę:", + "highDensity": "Wspieraj dużą gęstość pikseli (np. na wyświetlaczach retina)", + "maxFPS": "Maksymalna ilość klatek na sekundę:", + "usePixiLegacy": "Dodaj szeroko wspierany, bazujący na canvasie renderer żeby wspierać stare przeglądarki i karty graficzne (dodaje ~20kb do twojej gry)", + "pixelatedrender": "Zablokuj wygładzanie obrazu tu i w eksportowanym projekcie (zachowaj ostre piksele)" + }, "scripts": { "addNew": "Dodaj nowy skrypt", "deleteScript": "Usuń skrypt", - "header": "Skrypty", + "heading": "Skrypty", "newScriptComment": "Uzyj skryptów do definiowania często używanych funkcji i importowania małych bibliotek", "moveUp": "Przechodzenie w górę", "moveDown": "Przechodzenie w dół" diff --git a/app/data/i18n/Romanian.json b/app/data/i18n/Romanian.json index 93f7b658b..ea019982a 100644 --- a/app/data/i18n/Romanian.json +++ b/app/data/i18n/Romanian.json @@ -49,19 +49,6 @@ "zoomIn": "Mărire", "zoomOut": "Micșorare" }, - "actionsEditor": { - "actions": "Acțiuni", - "actionsEditor": "Editor de acțiuni", - "addAction": "Adaugă o acțiune", - "addMethod": "Adaugă o metodă de introducere", - "deleteAction": "Șterge această acțiune", - "deleteMethod": "Șterge această metodă", - "inputActionNamePlaceholder": "Numele acțiunii", - "methodModuleMissing": "Modulul necesar pentru această metodă lipsește", - "methods": "Metode de introducere", - "multiplier": "Coeficient", - "noActionsYet": "Acțiunile le permit dezvoltatorilor să captureze numeroase metode de introducere în același timp și să le schimbe dinamic, totul cu un singur API uniform. Citește mai mult făcând click pe iconița pentru documentație de mai sus." - }, "colorPicker": { "current": "Nou", "globalPalette": "Paletă globală", @@ -80,9 +67,6 @@ "firstrunnotice": "Prima rulare pentru fiecare platformă va fi lentă deoarece ct.js va descărca și salva bibliotecile necesare pentru împachetare. Va dura ceva timp, dar data viitoare va fi aproape instant.", "log": "Jurnal de mesaje" }, - "inputMethodSelector": { - "select": "Selectează" - }, "intro": { "loading": "Vă rugăm așteptați: pisicuțele accelerează la viteza luminii!", "newProject": { @@ -145,33 +129,40 @@ "codeDense": "" }, "settings": { - "actions": "Acțiuni și metode de introducere", - "author": "Numele autorului:", - "authoring": "Informații despre autor", - "cover": "Cover:", - "editActions": "Editează acțiunile", - "exportparams": "Exportă parametrii", - "framerate": "Framerate:", - "getfile": "Alege", - "minifyhtmlcss": "Comprimă HTML și CSS", - "minifyjs": "Comprimă JavaScript și convertește în ES5 (lent; folosește pentru lansări)", - "pixelatedrender": "Dezactivează netezirea imaginii aici și în proiectul exportat (conservă pixelii clari)", - "renderoptions": "Opțiuni de randare", - "settings": "Setările proiectului", - "site": "Pagină principală:", - "title": "Nume:", - "version": "Versiune:", - "versionpostfix": "Postfix:", + "actions": { + "heading": "Acțiuni și metode de introducere", + "actions": "Acțiuni", + "addAction": "Adaugă o acțiune", + "addMethod": "Adaugă o metodă de introducere", + "deleteAction": "Șterge această acțiune", + "deleteMethod": "Șterge această metodă", + "inputActionNamePlaceholder": "Numele acțiunii", + "methodModuleMissing": "Modulul necesar pentru această metodă lipsește", + "methods": "Metode de introducere", + "multiplier": "Coeficient", + "noActionsYet": "Acțiunile le permit dezvoltatorilor să captureze numeroase metode de introducere în același timp și să le schimbe dinamic, totul cu un singur API uniform. Citește mai mult făcând click pe iconița pentru documentație de mai sus." + }, + "authoring": { + "heading": "Informații despre autor", + "author": "Numele autorului:", + "site": "Pagină principală:", + "title": "Nume:", + "version": "Versiune:", + "versionpostfix": "Postfix:" + }, + "rendering": { + "heading": "Opțiuni de randare", + "framerate": "Framerate:", + "pixelatedrender": "Dezactivează netezirea imaginii aici și în proiectul exportat (conservă pixelii clari)" + }, "scripts": { + "heading": "Script-uri", "addNew": "Adaugă un nou script", "deleteScript": "Șterge script-ul", - "header": "Script-uri", "newScriptComment": "Folosește script-uri pentru a defini funcții frecvente și pentru a importa biblioteci mici", "moveUp": "Mută-te în sus.", "moveDown": "Mutare în jos" - }, - "highDensity": "", - "maxFPS": "" + } }, "modules": { "author": "Autorul acestui catmod", diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 27298e167..3f12e5832 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -51,19 +51,6 @@ "zoomIn": "Приблизить", "zoomOut": "Уменьшить" }, - "actionsEditor": { - "actions": "Действия", - "actionsEditor": "Редактор действий", - "addAction": "Добавить действие", - "addMethod": "Добавить метод ввода", - "deleteAction": "Удалить это действие", - "deleteMethod": "Удалить этот метод", - "inputActionNamePlaceholder": "Название действия", - "methodModuleMissing": "Модуль для этого метода ввода отсутствует", - "methods": "Методы ввода", - "multiplier": "Множитель", - "noActionsYet": "Действия позволяют разработчику реагировать на события различных методов ввода одновременно, а также динамически менять настройки управления — всё через единый API. Узнайте о них больше, нажав иконку документации выше." - }, "colorPicker": { "old": "Старый", "current": "Новый", @@ -82,17 +69,8 @@ "firstrunnotice": "В первый запуск, ct.js скачает дополнительные файлы для каждой платформы. Это займёт время, но после этот процесс будет практически мгновенным.", "log": "Логи", "windowsCrossBuildWarning": "Чтобы паковать игры для Windows с Линукса или MacOS, нужно установить Wine. Инструкции по установке Wine разные от системы к системе, так что лучше погуглите сами :)", - "launchMode": "Запустить в режиме:", - "launchModes": { - "maximized": "Развёрнутый на весь экран", - "fullscreen": "Полноэкранный", - "windowed": "Оконный" - }, "cannotBuildForMacOnWin": "К сожалению, Windows может делать для маков только сломанные пакеты. Попробуйте сделать билд на линуксе — например, в виртуалке. Это бесплатно!" }, - "inputMethodSelector": { - "select": "Выбрать" - }, "intro": { "loading": "Подождите, коты набирают скорость света…", "loadingProject": "Загрузка проекта…", @@ -138,6 +116,7 @@ "rooms": "Комнаты", "save": "Сохранить проект", "startScreen": "К стартовому экрану", + "project": "Проект", "settings": "Настройки", "sounds": "Звуки", "successZipExport": "Успешно экспортировано в {0}.", @@ -179,41 +158,57 @@ "showOnboardingCheckbox": "Показывать этот экран при создании новых проектов" }, "settings": { - "actions": "Действия и методы ввода", - "author": "Автор:", - "authoring": "Авторство", - "cover": "Обложка:", - "editActions": "Редактировать действия", - "exportparams": "Настройки экспорта", - "framerate": "Кадров в секунду:", - "getfile": "Выбрать", - "highDensity": "Поддерживать высокую плотность пикселей (напр. на ретина-экранах)", - "maxFPS": "Максмальная частота кадров:", - "minifyhtmlcss": "Сжать HTML и CSS", - "minifyjs": "Сжать JavaScript и преобразовать в ES5 (медленная операция, используйте для релиза)", - "usePixiLegacy": "Добавить рендерер на HTMLCanvas для поддержки старых браузеров и видеокарт (добавляет ~20kb к весу игры)", - "pixelatedrender": "Здесь и в проекте отключать сглаживание (сохранять пиксели)", - "renderoptions": "Настройки графики", - "settings": "Настройки проекта", - "site": "Сайт автора:", - "title": "Название:", - "version": "Версия:", - "versionpostfix": "Постфикс:", + "actions": { + "heading": "Действия и методы ввода", + "actions": "Действия", + "addAction": "Добавить действие", + "addMethod": "Добавить метод ввода", + "deleteAction": "Удалить это действие", + "deleteMethod": "Удалить этот метод", + "inputActionNamePlaceholder": "Название действия", + "methodModuleMissing": "Модуль для этого метода ввода отсутствует", + "methods": "Методы ввода", + "multiplier": "Множитель", + "noActionsYet": "Действия позволяют разработчику реагировать на события различных методов ввода одновременно, а также динамически менять настройки управления — всё через единый API. Узнайте о них больше, нажав иконку документации выше." + }, + "authoring": { + "heading": "Авторство", + "author": "Автор:", + "site": "Сайт автора:", + "title": "Название:", + "version": "Версия:", + "versionpostfix": "Постфикс:" + }, "branding": { "heading": "Брендинг", - "icon": "Иконка игры:", - "iconNotice": "Это должна быть квадратная текстура с одним кадром и размером минимум 256x256 пикселей.", "accent": "Акцентный цвет:", "accentNotice": "Определяет цвет экрана загрузки, а также некоторых мест на мобильных устройствах.", + "icon": "Иконка игры:", + "iconNotice": "Это должна быть квадратная текстура с одним кадром и размером минимум 256x256 пикселей.", "invertPreloaderScheme": "Инвертировать цветовую схему экрана загрузки" }, + "rendering": { + "heading": "Настройки графики", + "framerate": "Кадров в секунду:", + "highDensity": "Поддерживать высокую плотность пикселей (напр. на ретина-экранах)", + "maxFPS": "Максмальная частота кадров:", + "pixelatedrender": "Здесь и в проекте отключать сглаживание (сохранять пиксели)", + "usePixiLegacy": "Добавить рендерер на HTMLCanvas для поддержки старых браузеров и видеокарт (добавляет ~20kb к весу игры)", + "desktopBuilds": "Сборки для ПК", + "launchMode": "Запустить в режиме:", + "launchModes": { + "maximized": "Развёрнутый на весь экран", + "fullscreen": "Полноэкранный", + "windowed": "Оконный" + } + }, "scripts": { + "heading": "Пользовательские скрипты", "addNew": "Добавить новый скрипт", "deleteScript": "Удалить этот скрипт", - "header": "Скрипты", - "newScriptComment": "Используйте скрипты для создания функций и импорта небольших библиотек", + "moveDown": "Поставить ниже", "moveUp": "Поставить выше", - "moveDown": "Поставить ниже" + "newScriptComment": "Используйте скрипты для создания функций и импорта небольших библиотек" } }, "modules": { diff --git a/app/data/i18n/Spanish.json b/app/data/i18n/Spanish.json index 2ad096f37..3dbabe840 100644 --- a/app/data/i18n/Spanish.json +++ b/app/data/i18n/Spanish.json @@ -49,19 +49,6 @@ "zoomIn": "Acercar", "zoomOut": "Alejar" }, - "actionsEditor": { - "actions": "Acciones", - "actionsEditor": "Editor de acciones", - "addAction": "Agregar una acción", - "addMethod": "Agrega un método de entrada", - "deleteAction": "Eliminar esta acción", - "deleteMethod": "Eliminar este método", - "inputActionNamePlaceholder": "Nombre de acción", - "methodModuleMissing": "Falta el módulo requerido para este método", - "methods": "Métodos de entrada", - "multiplier": "Multiplicador", - "noActionsYet": "Las acciones permiten a los desarrolladores escuchar numerosos métodos de entrada a la vez y cambiarlos dinámicamente, todo con una API uniforme. Lea más haciendo clic en el icono de documentos arriba." - }, "colorPicker": { "current": "Nuevo", "globalPalette": "Paleta Global", @@ -80,9 +67,6 @@ "firstrunnotice": "La primera ejecución para cada plataforma será lenta ya que ct.js descargará y guardará las bibliotecas adicionales necesarias para empacar. Tomará algún tiempo, pero las próximas veces serán casi instantáneas.", "log": "Registro de mensajes" }, - "inputMethodSelector": { - "select": "Seleccionar" - }, "intro": { "loading": "Por favor espere: ¡los gatitos están ganando velocidad con la luz!", "newProject": { @@ -145,33 +129,40 @@ "codeDense": "" }, "settings": { - "actions": "Acciones y métodos de entrada", - "author": "Nombre del autor:", - "authoring": "Autoría", - "cover": "Cubrir:", - "editActions": "Editar acciones", - "exportparams": "Parámetros de exportación", - "framerate": "Cuadros por segundo:", - "getfile": "Escoger", - "minifyhtmlcss": "Comprimir HTML y CSS", - "minifyjs": "Comprima JavaScript y conviértalo a ES5 (lento; uso para versiones)", - "pixelatedrender": "Deshabilite el suavizado de imagen aquí y en el proyecto exportado (conserve píxeles nítidos)", - "renderoptions": "Opciones de render", - "settings": "Configuración del proyecto", - "site": "Página principal:", - "title": "Nombre:", - "version": "Version:", - "versionpostfix": "Sufijo:", + "actions": { + "heading": "Acciones y métodos de entrada", + "actions": "Acciones", + "addAction": "Agregar una acción", + "addMethod": "Agrega un método de entrada", + "deleteAction": "Eliminar esta acción", + "deleteMethod": "Eliminar este método", + "inputActionNamePlaceholder": "Nombre de acción", + "methodModuleMissing": "Falta el módulo requerido para este método", + "methods": "Métodos de entrada", + "multiplier": "Multiplicador", + "noActionsYet": "Las acciones permiten a los desarrolladores escuchar numerosos métodos de entrada a la vez y cambiarlos dinámicamente, todo con una API uniforme. Lea más haciendo clic en el icono de documentos arriba." + }, + "authoring": { + "heading": "Autoría", + "author": "Nombre del autor:", + "site": "Página principal:", + "title": "Nombre:", + "version": "Version:", + "versionpostfix": "Sufijo:" + }, + "rendering": { + "heading": "Opciones de render", + "framerate": "Cuadros por segundo:", + "pixelatedrender": "Deshabilite el suavizado de imagen aquí y en el proyecto exportado (conserve píxeles nítidos)" + }, "scripts": { + "heading": "Scripts", "addNew": "Agregar un nuevo script", "deleteScript": "Eliminar el script", - "header": "Scripts", "newScriptComment": "Use scripts para definir funciones frecuentes e importar pequeñas bibliotecas", "moveUp": "Subir", "moveDown": "Muévete hacia abajo" - }, - "highDensity": "", - "maxFPS": "" + } }, "modules": { "author": "Autor de este catmod", diff --git a/src/icons/sliders.svg b/src/icons/sliders.svg new file mode 100644 index 000000000..235cc614e --- /dev/null +++ b/src/icons/sliders.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/js/migration/1.3.2.js b/src/js/migration/1.3.2.js new file mode 100644 index 000000000..462a2f370 --- /dev/null +++ b/src/js/migration/1.3.2.js @@ -0,0 +1,43 @@ +window.migrationProcess = window.migrationProcess || []; + +/** + * Project settings got reorganized, logically and visually + */ +window.migrationProcess.push({ + version: '1.3.2', + process: project => new Promise(resolve => { + const s = project.settings; + + s.rendering = s.rendering || { + maxFPS: s.maxFPS, + pixelatedrender: s.pixelatedrender, + highDensity: s.highDensity, + usePixiLegacy: s.usePixiLegacy, + desktopMode: s.desktopMode + }; + delete s.maxFPS; + delete s.fps; // A legacy config that was relevant prior to v1. + delete s.pixelatedrender; + delete s.highDensity; + delete s.usePixiLegacy; + delete s.desktopMode; + + s.authoring = s.authoring || { + title: s.title, + author: s.author, + site: s.site, + version: s.version, + versionPostfix: s.versionPostfix + }; + delete s.title; + delete s.author; + delete s.site; + delete s.version; + delete s.versionPostfix; + + delete s.minifyhtmlcss; // Now doesn't have an option, HTML and CSS are always minified. + delete s.minifyjs; // This one never worked properly. + + resolve(); + }) +}); diff --git a/src/node_requires/exporter/icons.js b/src/node_requires/exporter/icons.js index c19b72a4d..66caf9798 100644 --- a/src/node_requires/exporter/icons.js +++ b/src/node_requires/exporter/icons.js @@ -32,13 +32,13 @@ const bakeFavicons = async function (proj, writeDir) { const img = await getDOMImage(proj.settings.branding.icon, 'ct_ide.png'), fsPath = proj.settings.branding.icon ? getTextureOrig(proj.settings.branding.icon, true) : path.resolve('ct_ide.png'); const promises = []; - const soft = !proj.settings.pixelatedrender; + const soft = !proj.settings.rendering.pixelatedrender; for (const name in iconMap) { for (const size of iconMap[name]) { promises.push(resizeTo(img, size, path.join(writeDir, `${name}-${size}x${size}.png`), soft)); } } - const interpolation = proj.settings.pixelatedrender ? png2icons.BILINEAR : png2icons.HERMITE; + const interpolation = soft ? png2icons.HERMITE : png2icons.BILINEAR; promises.push(fs.readFile(fsPath) .then(buff => png2icons.createICO(buff, interpolation)) .then(buff => fs.outputFile(path.join(writeDir, 'favicon.ico'), buff))); diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index 71d1f8df5..33cefc8fe 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -72,10 +72,10 @@ const makeWritableDir = async () => { // eslint-disable-next-line max-lines-per-function const exportCtProject = async (project, projdir) => { const {languageJSON} = require('./../i18n'); - currentProject = project; + const {settings} = project; await makeWritableDir(); - if (currentProject.rooms.length < 1) { + if (project.rooms.length < 1) { throw new Error(languageJSON.common.norooms); } @@ -119,16 +119,16 @@ const exportCtProject = async (project, projdir) => { await injectModules(injects); /* Pixi.js */ - if (currentProject.settings.usePixiLegacy) { + if (settings.rendering.usePixiLegacy) { await fs.copyFile(basePath + 'ct.release/pixi-legacy.min.js', path.join(writeDir, '/pixi.min.js')); } else { await fs.copyFile(basePath + 'ct.release/pixi.min.js', path.join(writeDir, '/pixi.min.js')); } - if (currentProject.emitterTandems && currentProject.emitterTandems.length) { + if (project.emitterTandems && project.emitterTandems.length) { await fs.copyFile(basePath + 'ct.release/pixi-particles.min.js', path.join(writeDir, '/pixi-particles.min.js')); } - const startroom = getStartingRoom(currentProject); + const startroom = getStartingRoom(project); /* Load source files in parallel */ const sources = {}; @@ -155,21 +155,21 @@ const exportCtProject = async (project, projdir) => { let buffer = (await sources['main.js']) .replace(/\/\*@startwidth@\*\//g, startroom.width) .replace(/\/\*@startheight@\*\//g, startroom.height) - .replace(/\/\*@pixelatedrender@\*\//g, Boolean(currentProject.settings.pixelatedrender)) - .replace(/\/\*@highDensity@\*\//g, Boolean(currentProject.settings.highDensity)) - .replace(/\/\*@maxfps@\*\//g, Number(currentProject.settings.maxFPS)) + .replace(/\/\*@pixelatedrender@\*\//g, Boolean(settings.rendering.pixelatedrender)) + .replace(/\/\*@highDensity@\*\//g, Boolean(settings.rendering.highDensity)) + .replace(/\/\*@maxfps@\*\//g, Number(settings.rendering.maxFPS)) .replace(/\/\*@ctversion@\*\//g, process.versions.ctjs) .replace(/\/\*@projectmeta@\*\//g, JSON.stringify({ - name: currentProject.settings.title, - author: currentProject.settings.author, - site: currentProject.settings.site, - version: currentProject.settings.version.join('.') + currentProject.settings.versionPostfix + name: settings.authoring.title, + author: settings.authoring.author, + site: settings.authoring.site, + version: settings.authoring.version.join('.') + settings.authoring.versionPostfix })); buffer += '\n'; let actionsSetup = ''; - for (const action of currentProject.actions) { + for (const action of project.actions) { actionsSetup += `ct.inputs.addAction('${action.name}', ${JSON.stringify(action.methods)});\n`; } @@ -180,11 +180,11 @@ const exportCtProject = async (project, projdir) => { buffer += await addModules(buffer); /* User-defined scripts */ - for (const script of currentProject.scripts) { + for (const script of project.scripts) { buffer += script.code + ';\n'; } - const roomsCode = stringifyRooms(currentProject); + const roomsCode = stringifyRooms(project); buffer += (await sources['rooms.js']) .replace('@startroom@', startroom.name) @@ -195,28 +195,28 @@ const exportCtProject = async (project, projdir) => { .replace('/*%roomonleave%*/', injects.roomonleave); buffer += '\n'; - const styles = stringifyStyles(currentProject); + const styles = stringifyStyles(project); buffer += (await sources['styles.js']) .replace('/*@styles@*/', styles) .replace('/*%styles%*/', injects.styles); buffer += '\n'; - if (currentProject.emitterTandems && currentProject.emitterTandems.length) { - const templates = stringifyTandems(currentProject); + if (project.emitterTandems && project.emitterTandems.length) { + const templates = stringifyTandems(project); buffer += (await sources['emitters.js']) .replace('/*@tandemTemplates@*/', templates); } /* assets — run in parallel */ - const texturesTask = packImages(currentProject, writeDir); - const skeletonsTask = packSkeletons(currentProject, projdir, writeDir); - const favicons = bakeFavicons(currentProject, writeDir); + const texturesTask = packImages(project, writeDir); + const skeletonsTask = packSkeletons(project, projdir, writeDir); + const favicons = bakeFavicons(project, writeDir); const textures = await texturesTask; const skeletons = await skeletonsTask; await favicons; buffer += (await sources['res.js']) - .replace('/*@sndtotal@*/', currentProject.sounds.length) + .replace('/*@sndtotal@*/', project.sounds.length) .replace('/*@res@*/', textures.res + '\n' + skeletons.loaderScript) .replace('/*@textureregistry@*/', textures.registry) .replace('/*@textureatlases@*/', JSON.stringify(textures.atlases)) @@ -225,7 +225,7 @@ const exportCtProject = async (project, projdir) => { .replace('/*%res%*/', injects.res); buffer += '\n'; - const types = stringifyTypes(currentProject); + const types = stringifyTypes(project); buffer += (await sources['types.js']) .replace('/*%oncreate%*/', injects.oncreate) @@ -236,14 +236,14 @@ const exportCtProject = async (project, projdir) => { buffer += '\n'; buffer += '\n'; - var sounds = stringifySounds(currentProject); + var sounds = stringifySounds(project); buffer += (await sources['sound.js']) .replace('/*@sound@*/', sounds); buffer += (await sources['timer.js']); buffer += '\n'; - const fonts = await bundleFonts(currentProject, projdir, writeDir); + const fonts = await bundleFonts(project, projdir, writeDir); buffer += fonts.js; /* eslint-enable require-atomic-updates */ @@ -251,7 +251,7 @@ const exportCtProject = async (project, projdir) => { if (await fs.exists(projdir + '/include/')) { await fs.copy(projdir + '/include/', writeDir); } - await Promise.all(Object.keys(currentProject.libs).map(async lib => { + await Promise.all(Object.keys(project.libs).map(async lib => { if (await fs.exists(path.join(basePath, `./ct.libs/${lib}/includes/`))) { await fs.copy(path.join(basePath, `./ct.libs/${lib}/includes/`), writeDir); } @@ -262,83 +262,36 @@ const exportCtProject = async (project, projdir) => { buffer = buffer.replace(`/*%${i}%*/`, injects[i]); } - /* Final touches and script output */ - if (currentProject.settings.minifyjs) { - const preamble = '/* Made with ct.js http://ctjs.rocks/ */\n'; - const ClosureCompiler = require('google-closure-compiler').jsCompiler; - - const compiler = new ClosureCompiler({ - /* eslint-disable camelcase */ - compilation_level: 'SIMPLE', - // eslint-disable-next-line id-length - use_types_for_optimization: false, - jscomp_off: '*', // Disable warnings to not to booo users - language_out: 'ECMASCRIPT3', - language_in: 'ECMASCRIPT_NEXT', - warning_level: 'QUIET' - /* eslint-enable camelcase */ - }); - const out = await new Promise((resolve, reject) => { - compiler.run([{ - path: path.join(writeDir, '/ct.js'), - src: buffer - }], (exitCode, stdout, stderr) => { - if (stderr && !stdout) { - reject(stderr); - return; - } - resolve(stdout[0]); - if (stderr) { - console.error(stderr); - } - }); - }); - await fs.writeFile(path.join(writeDir, '/ct.js'), preamble + out.src); - await fs.writeFile(path.join(writeDir, '/ct.js.map'), out.sourceMap); - } else { - await fs.writeFile(path.join(writeDir, '/ct.js'), buffer); - } /* HTML & CSS */ const {substituteHtmlVars} = require('./html'); const {substituteCssVars} = require('./css'); - const html = substituteHtmlVars(await sources['index.html'], currentProject, injects); - - let css = substituteCssVars(await sources['ct.css'], currentProject, injects); + const html = substituteHtmlVars(await sources['index.html'], project, injects); + let css = substituteCssVars(await sources['ct.css'], project, injects); css += fonts.css; + // Output minified HTML & CSS + const csswring = require('csswring'); + const htmlMinify = require('html-minifier').minify; await Promise.all([ - fs.writeFile(path.join(writeDir, '/index.html'), html), - fs.writeFile(path.join(writeDir, '/ct.css'), css) + fs.writeFile( + path.join(writeDir, '/index.html'), + htmlMinify(html, { + removeComments: true, + collapseWhitespace: true + }) + ), + fs.writeFile(path.join(writeDir, '/ct.css'), csswring.wring(css).css), + fs.writeFile(path.join(writeDir, '/ct.js'), buffer) ]); - if (currentProject.settings.minifyhtmlcss) { - const csswring = require('csswring'); - const htmlMinify = require('html-minifier').minify; - const htmlUnminified = fs.readFile(path.join(writeDir, '/index.html'), { - encoding: 'utf8' - }); - const cssUnminified = fs.readFile(path.join(writeDir, '/ct.css'), { - encoding: 'utf8' - }); - await Promise.all([ - fs.writeFile( - path.join(writeDir, '/index.html'), - htmlMinify(await htmlUnminified, { - removeComments: true, - collapseWhitespace: true - }) - ), - fs.writeFile(path.join(writeDir, '/ct.css'), csswring.wring(await cssUnminified).css) - ]); - } - await Promise.all(currentProject.sounds.map(async sound => { + await Promise.all(project.sounds.map(async sound => { const ext = sound.origname.slice(-4); await fs.copy(path.join(projdir, '/snd/', sound.origname), path.join(writeDir, '/snd/', sound.uid + ext)); })); - return path.join(writeDir, `/index.${currentProject.settings.minifyhtml ? 'min.' : ''}html`); + return path.join(writeDir, '/index.html'); }; module.exports = exportCtProject; diff --git a/src/node_requires/resources/projects/defaultProject.js b/src/node_requires/resources/projects/defaultProject.js index b51e2de3c..90a4dadf7 100644 --- a/src/node_requires/resources/projects/defaultProject.js +++ b/src/node_requires/resources/projects/defaultProject.js @@ -24,12 +24,20 @@ const defaultProjectTemplate = { emitterTandems: [], starting: 0, settings: { - minifyhtmlcss: false, - minifyjs: false, - fps: 60, - version: [0, 0, 0], - versionPostfix: '', - usePixiLegacy: true, + authoring: { + author: '', + site: '', + title: '', + version: [0, 0, 0], + versionPostfix: '' + }, + rendering: { + usePixiLegacy: true, + maxFPS: 60, + pixelatedrender: false, + highDensity: true, + desktopMode: 'maximized' + }, export: { windows: true, linux: true, diff --git a/src/riotTags/actions-editor.tag b/src/riotTags/actions-editor.tag deleted file mode 100644 index 0828134f2..000000000 --- a/src/riotTags/actions-editor.tag +++ /dev/null @@ -1,116 +0,0 @@ -actions-editor.panel.view.pad - .panel.pad.flexfix.tall - .flexfix-header - h1 - | {voc.actionsEditor} - docs-shortcut(path="/actions.html") - p(if="{!global.currentProject.actions || !global.currentProject.actions.length}") {voc.noActionsYet} - .flexfix-body(if="{!global.currentProject.actions || !global.currentProject.actions.length}") - button.nml(onclick="{addNewAction}") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {vocGlob.add} - .flexfix-body.aStrippedList.nmt(if="{global.currentProject.actions && global.currentProject.actions.length}") - li.hide800.npl.npr - .c4.npt.npb.npl - b {voc.actions} - .c8.npt.npb.npr - b {voc.methods} - .clear - li.npl.npt(each="{action, ind in global.currentProject.actions}") - .c4.npl.breakon800 - .flexrow.middle - div.relative.wide - input.wide(type="text" placeholder="{voc.inputActionNamePlaceholder}" value="{action.name}" onchange="{checkActionNameAndSave}") - .anErrorNotice(if="{nameTaken === action.name}" ref="errors") {vocGlob.nametaken} - .anErrorNotice(if="{action.name.trim() === ''}" ref="errors") {vocGlob.cannotBeEmpty} - .spacer - svg.feather.a(title="{voc.deleteAction}" onclick="{deleteAction}") - use(xlink:href="data/icons.svg#x") - .c8.npr.breakon800 - ul.aStrippedList.nmt - li.flexrow.middle.npl(each="{method, mInd in action.methods}") - .fifty.npt.npl.npb - code.inline {method.code} - svg.feather.orange(if="{!(method.code.split('.')[0] in global.currentProject.libs)}" title="{voc.methodModuleMissing}") - use(xlink:href="data/icons.svg#alert-circle") - .fifty.npt.npr.npb - b {voc.multiplier}: - input.short( - type="number" step="0.1" - value="{method.multiplier === void 0? 1 : method.multiplier}" - onchange="{wire('global.currentProject.actions.'+ ind +'.methods.'+ mInd +'.multiplier')}" - ) - svg.feather.a(title="{voc.deleteMethod}" onclick="{deleteMethod(action)}") - use(xlink:href="data/icons.svg#x") - button.nml(onclick="{addMethod}") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.addMethod} - .clear - p - button.nml(onclick="{addNewAction}") - svg.feather - use(xlink:href="data/icons.svg#plus") - span {voc.addAction} - .flexfix-footer - button.wide(onclick="{saveActions}") - svg.feather - use(xlink:href="data/icons.svg#save") - span {vocGlob.save} - .dimmer(show="{addingMethod}") - method-selector(action="{editedAction}" ref="methodSelector") - script. - global.currentProject.actions = global.currentProject.actions || []; - this.namespace = 'actionsEditor'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.addNewAction = () => { - global.currentProject.actions.push({ - name: 'NewAction', - methods: [] - }); - }; - this.deleteAction = e => { - const ind = global.currentProject.actions.indexOf(e.item.action); - global.currentProject.actions.splice(ind, 1); - }; - this.addMethod = e => { - this.addingMethod = true; - this.editedAction = e.item.action; - setTimeout(() => { - this.refs.methodSelector.refs.searchField.focus(); - }, 0); - }; - this.deleteMethod = action => e => { - const ind = action.methods.indexOf(e.item.method); - action.methods.splice(ind, 1); - }; - this.checkActionNameAndSave = e => { - this.nameTaken = void 0; - e.item.action.name = e.currentTarget.value.trim(); - const existingAction = global.currentProject.actions.find(action => - action !== e.item.action && action.name === e.item.action.name); - if (existingAction) { - this.nameTaken = e.item.action.name; - } - }; - - this.saveActions = () => { - if ((Array.isArray(this.refs.errors) && this.refs.errors.length) || this.refs.errors) { - let {errors} = this.refs; - if (!Array.isArray(errors)) { - errors = [errors]; - } - // animate the error notice - const jellify = require('./data/node_requires/jellify'); - for (const errorTag of errors) { - jellify(errorTag); - } - soundbox.play('Failure'); - return false; - } - this.parent.editingActions = false; - this.parent.update(); - return true; - }; \ No newline at end of file diff --git a/src/riotTags/export-panel.tag b/src/riotTags/export-panel.tag index 901a1c0a1..32cb5e88c 100644 --- a/src/riotTags/export-panel.tag +++ b/src/riotTags/export-panel.tag @@ -7,27 +7,21 @@ export-panel p {voc.firstrunnotice} fieldset label.checkbox - input(type="checkbox" checked="{global.currentProject.settings.export.linux}" onchange="{wire('global.currentProject.settings.export.linux')}") + input(type="checkbox" checked="{projSettings.export.linux}" onchange="{wire('projSettings.export.linux')}") svg.icon use(xlink:href="data/icons.svg#linux") | Linux label.checkbox(disabled="{process.platform === 'win32'}" title="{process.platform === 'win32' && voc.cannotBuildForMacOnWin}") - input(type="checkbox" checked="{global.currentProject.settings.export.mac}" onchange="{wire('global.currentProject.settings.export.mac')}") + input(type="checkbox" checked="{projSettings.export.mac}" onchange="{wire('projSettings.export.mac')}") svg.icon use(xlink:href="data/icons.svg#apple") | MacOS label.checkbox - input(type="checkbox" checked="{global.currentProject.settings.export.windows}" onchange="{wire('global.currentProject.settings.export.windows')}") + input(type="checkbox" checked="{projSettings.export.windows}" onchange="{wire('projSettings.export.windows')}") svg.icon use(xlink:href="data/icons.svg#windows") | Windows - fieldset - b {voc.launchMode} - each key in ['maximized', 'fullscreen', 'windowed'] - label.checkbox - input(type="radio" value=key checked=`{global.currentProject.settings.desktopMode === '${key}'}` onchange="{wire('global.currentProject.settings.desktopMode')}") - span=`{voc.launchModes.${key}}` - p.warning(if="{global.currentProject.settings.export.windows && process.platform !== 'win32'}") + p.warning(if="{projSettings.export.windows && process.platform !== 'win32'}") svg.feather use(xlink:href="data/icons.svg#alert-triangle") | @@ -57,7 +51,11 @@ export-panel this.mixin(window.riotWired); this.working = false; this.log = []; + global.currentProject.settings.export = global.currentProject.settings.export || {}; + const projSettings = global.currentProject.settings; + this.projSettings = global.currentProject.settings; + this.closeExporter = function closeExporter() { this.parent.showExporter = false; @@ -69,15 +67,15 @@ export-panel const png2icons = require('png2icons'), {getTextureOrig} = require('./data/node_requires/resources/textures'); - const iconPath = getTextureOrig(global.currentProject.settings.branding.icon || -1, true), + const iconPath = getTextureOrig(projSettings.branding.icon || -1, true), icon = await fs.readFile(iconPath); await fs.outputFile(path.join(exportDir, 'icon.icns'), png2icons.createICNS( icon, - global.currentProject.settings.pixelatedrender ? png2icons.BILINEAR : png2icons.HERMITE + projSettings.rendering.pixelatedrender ? png2icons.BILINEAR : png2icons.HERMITE )); await fs.outputFile(path.join(exportDir, 'icon.ico'), png2icons.createICO( icon, - global.currentProject.settings.antialias ? png2icons.BILINEAR : png2icons.HERMITE, + projSettings.rendering.antialias ? png2icons.BILINEAR : png2icons.HERMITE, 0, true, true )); }; @@ -112,17 +110,17 @@ export-panel this.log.push('Preparing build metadata…'); this.update(); const packageJson = fs.readJSONSync('./data/ct.release/desktopPack/package.json'); - const version = global.currentProject.settings.version.join('.') + global.currentProject.settings.versionPostfix; + const version = projSettings.authoring.version.join('.') + projSettings.authoring.versionPostfix; packageJson.version = version; - if (global.currentProject.settings.title) { - packageJson.name = global.currentProject.settings.title; - packageJson.window.title = global.currentProject.settings.title; + if (projSettings.authoring.title) { + packageJson.name = projSettings.authoring.title; + packageJson.window.title = projSettings.authoring.title; } const startingRoom = global.currentProject.rooms.find(room => room.uid === global.currentProject.startroom) || global.currentProject.rooms[0]; packageJson.window.width = startingRoom.width; packageJson.window.height = startingRoom.height; - packageJson.window.mode = global.currentProject.settings.desktopMode || 'maximized'; + packageJson.window.mode = projSettings.rendering.desktopMode || 'maximized'; await fs.outputJSON(path.join(exportDir, 'package.json'), packageJson); this.log.push('Baking icons…'); @@ -145,11 +143,11 @@ export-panel appCategoryType: 'public.app-category.games', // Game-specific metadata - executableName: global.currentProject.settings.title || 'ct.js game', - appCopyright: global.currentProject.settings.author && `Copyright © ${global.currentProject.settings.author} ${(new Date()).getFullYear()}`, - win32metadata: global.currentProject.settings.author && { - CompanyName: global.currentProject.settings.author, - FileDescription: global.currentProject.settings.description || 'A cool game made in ct.js game editor' + executableName: projSettings.authoring.title || 'ct.js game', + appCopyright: projSettings.authoring.author && `Copyright © ${projSettings.authoring.author} ${(new Date()).getFullYear()}`, + win32metadata: projSettings.authoring.author && { + CompanyName: projSettings.authoring.author, + FileDescription: projSettings.authoring.description || 'A cool game made in ct.js game editor' } }; @@ -165,7 +163,7 @@ export-panel windows: 'win32' }; for (const settingKey in platformMap) { - if (!global.currentProject.settings.export[settingKey]) { + if (!projSettings.export[settingKey]) { continue; } const platform = platformMap[settingKey]; diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 4ece0fa42..0c6f8c6c3 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -21,10 +21,10 @@ main-menu.flexcol use(xlink:href="data/icons.svg#play") ul#mainnav.nav.tabs - li(onclick="{changeTab('settings')}" class="{active: tab === 'settings'}" data-hotkey="Control+1" title="Control+1") + li(onclick="{changeTab('project')}" class="{active: tab === 'project'}" data-hotkey="Control+1" title="Control+1") svg.feather - use(xlink:href="data/icons.svg#settings") - span {voc.settings} + use(xlink:href="data/icons.svg#sliders") + span {voc.project} li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}" data-hotkey="Control+2" title="Control+2") svg.feather use(xlink:href="data/icons.svg#ctmod") @@ -54,7 +54,7 @@ main-menu.flexcol use(xlink:href="data/icons.svg#room") span {voc.rooms} div.flexitem.relative(if="{global.currentProject}") - settings-panel(show="{tab === 'settings'}" data-hotkey-scope="settings") + project-settings(show="{tab === 'project'}" data-hotkey-scope="project") modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") ui-panel(show="{tab === 'ui'}" data-hotkey-scope="ui") @@ -81,7 +81,7 @@ main-menu.flexcol this.namespace = 'menu'; this.mixin(window.riotVoc); - this.tab = 'settings'; + this.tab = 'project'; this.changeTab = tab => () => { this.tab = tab; hotkey.cleanScope(); diff --git a/src/riotTags/method-selector.tag b/src/riotTags/project-settings/actions-input-selector.tag similarity index 93% rename from src/riotTags/method-selector.tag rename to src/riotTags/project-settings/actions-input-selector.tag index d8d4cfc94..54549ce1c 100644 --- a/src/riotTags/method-selector.tag +++ b/src/riotTags/project-settings/actions-input-selector.tag @@ -1,13 +1,15 @@ -method-selector +actions-input-selector .panel.flexfix .flexfix-header .aSearchWrap.wide input.wide( - type="text" + type="text" ref="searchField" value="{searchString}" onkeyup="{wire('this.searchString')}" ) + svg.feather + use(xlink:href="data/icons.svg#search") .flexfix-body virtual(each="{module in inputProviders}") h2 {module.name} @@ -24,11 +26,11 @@ method-selector .flexfix-footer .flexrow button.nml.secondary(onclick="{cancel}") - span {vocGlob.cancel} + span {voc.cancel} button.nml.secondary(onclick="{apply}" disabled="{!selectedMethod}") span {voc.select} script. - this.namespace = 'inputMethodSelector'; + this.namespace = 'common'; this.mixin(window.riotVoc); this.mixin(window.riotWired); diff --git a/src/riotTags/project-settings/actions-settings.tag b/src/riotTags/project-settings/actions-settings.tag new file mode 100644 index 000000000..fbc28ff58 --- /dev/null +++ b/src/riotTags/project-settings/actions-settings.tag @@ -0,0 +1,90 @@ +actions-settings + h1 + | {voc.heading} + docs-shortcut(path="/actions.html") + p(if="{!global.currentProject.actions || !global.currentProject.actions.length}") {voc.noActionsYet} + div(if="{!global.currentProject.actions || !global.currentProject.actions.length}") + button.nml(onclick="{addNewAction}") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {vocGlob.add} + div(if="{global.currentProject.actions && global.currentProject.actions.length}") + li.hide800.npl.npr + .c4.npt.npb.npl + b {voc.actions} + .c8.np + b {voc.methods} + .clear + li.npl.npt(each="{action, ind in global.currentProject.actions}") + .c4.npl.breakon800 + .flexrow.middle + div.relative.wide + input.wide(type="text" placeholder="{voc.inputActionNamePlaceholder}" value="{action.name}" onchange="{checkActionNameAndSave}") + .anErrorNotice(if="{nameTaken === action.name}" ref="errors") {vocGlob.nametaken} + .anErrorNotice(if="{action.name.trim() === ''}" ref="errors") {vocGlob.cannotBeEmpty} + .spacer + svg.feather.a(title="{voc.deleteAction}" onclick="{deleteAction}") + use(xlink:href="data/icons.svg#x") + .c8.npr.npl.breakon800 + ul.aStrippedList.nmt + li.flexrow.middle.npl(each="{method, mInd in action.methods}") + .fifty.npt.npl.npb + code.inline {method.code} + svg.feather.orange(if="{!(method.code.split('.')[0] in global.currentProject.libs)}" title="{voc.methodModuleMissing}") + use(xlink:href="data/icons.svg#alert-circle") + .fifty.npt.npr.npb + b {voc.multiplier}: + input.short( + type="number" step="0.1" + value="{method.multiplier === void 0? 1 : method.multiplier}" + onchange="{wire('global.currentProject.actions.'+ ind +'.methods.'+ mInd +'.multiplier')}" + ) + svg.feather.a(title="{voc.deleteMethod}" onclick="{deleteMethod(action)}") + use(xlink:href="data/icons.svg#x") + button.nml(onclick="{addMethod}") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.addMethod} + .clear + p + button.nml(onclick="{addNewAction}") + svg.feather + use(xlink:href="data/icons.svg#plus") + span {voc.addAction} + .dimmer(show="{addingMethod}") + actions-input-selector(action="{editedAction}" ref="methodSelector") + script. + global.currentProject.actions = global.currentProject.actions || []; + this.namespace = 'settings.actions'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.addNewAction = () => { + global.currentProject.actions.push({ + name: 'NewAction', + methods: [] + }); + }; + this.deleteAction = e => { + const ind = global.currentProject.actions.indexOf(e.item.action); + global.currentProject.actions.splice(ind, 1); + }; + this.addMethod = e => { + this.addingMethod = true; + this.editedAction = e.item.action; + setTimeout(() => { + this.refs.methodSelector.refs.searchField.focus(); + }, 0); + }; + this.deleteMethod = action => e => { + const ind = action.methods.indexOf(e.item.method); + action.methods.splice(ind, 1); + }; + this.checkActionNameAndSave = e => { + this.nameTaken = void 0; + e.item.action.name = e.currentTarget.value.trim(); + const existingAction = global.currentProject.actions.find(action => + action !== e.item.action && action.name === e.item.action.name); + if (existingAction) { + this.nameTaken = e.item.action.name; + } + }; \ No newline at end of file diff --git a/src/riotTags/project-settings/authoring-settings.tag b/src/riotTags/project-settings/authoring-settings.tag new file mode 100644 index 000000000..89ccf875d --- /dev/null +++ b/src/riotTags/project-settings/authoring-settings.tag @@ -0,0 +1,37 @@ +authoring-settings + h1 {voc.heading} + b {voc.title} + br + input#gametitle(type="text" value="{authoring.title}" onchange="{changeTitle}") + br + b {voc.author} + br + input#gameauthor(type="text" value="{authoring.author}" onchange="{wire('this.authoring.author')}") + br + b {voc.site} + br + input#gamesite(type="text" value="{authoring.site}" onchange="{wire('this.authoring.site')}") + br + b {voc.version} + br + input(type="number" style="width: 1.5rem;" value="{authoring.version[0]}" length="3" min="0" onchange="{wire('this.authoring.version.0')}") + | . + input(type="number" style="width: 1.5rem;" value="{authoring.version[1]}" length="3" min="0" onchange="{wire('this.authoring.version.1')}") + | . + input(type="number" style="width: 1.5rem;" value="{authoring.version[2]}" length="3" min="0" onchange="{wire('this.authoring.version.2')}") + .inlineblock + | {voc.versionpostfix} + input(type="text" style="width: 3rem;" value="{authoring.versionPostfix}" length="5" onchange="{wire('this.authoring.versionPostfix')}") + script. + this.namespace = 'settings.authoring'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.currentProject = global.currentProject; + this.authoring = this.currentProject.settings.authoring; + + this.changeTitle = e => { + this.authoring.title = e.target.value.trim(); + if (this.authoring.title) { + document.title = this.authoring.title + ' — ct.js'; + } + }; \ No newline at end of file diff --git a/src/riotTags/project-settings/branding-settings.tag b/src/riotTags/project-settings/branding-settings.tag new file mode 100644 index 000000000..4ae1939f8 --- /dev/null +++ b/src/riotTags/project-settings/branding-settings.tag @@ -0,0 +1,32 @@ +branding-settings + h1 {voc.heading} + .block + b + span {voc.icon} + hover-hint(text="{voc.iconNotice}") + br + texture-input( + val="{global.currentProject.settings.branding.icon || -1}" + showempty="yep" + onselected="{updateGameIcon}" + header="{voc.icon}" + ) + .spacer + .block + b + span {voc.accent} + hover-hint(text="{voc.accentNotice}") + color-input(onchange="{wire('global.currentProject.settings.branding.accent', true)}" color="{global.currentProject.settings.branding.accent}") + .spacer + .block.checkbox + input(type="checkbox" value="{global.currentProject.settings.branding.invertPreloaderScheme}" checked="{global.currentProject.settings.branding.invertPreloaderScheme}" onchange="{wire('this.currentProject.settings.branding.invertPreloaderScheme')}") + span {voc.invertPreloaderScheme} + script. + this.namespace = 'settings.branding'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.currentProject = global.currentProject; + + this.updateGameIcon = tex => { + global.currentProject.settings.branding.icon = tex.uid; + }; \ No newline at end of file diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag new file mode 100644 index 000000000..50fa4f3e1 --- /dev/null +++ b/src/riotTags/project-settings/project-settings.tag @@ -0,0 +1,36 @@ +project-settings.panel.view + - + var tabs = ['authoring', 'actions', 'branding', 'rendering', 'scripts']; + var iconMap = { + authoring: 'edit', + actions: 'airplay', + branding: 'droplet', + rendering: 'room', + scripts: 'terminal', + default: 'settings' + }; + aside + ul.nav.tabs.vertical + // A bit of Pug sorcery, destroyed by Riot.js syntax + // Iterate over an array of sections. Template out Riot syntax inside `these` backticks. + each name in tabs + li(onclick=`{openTab('${name}')}` class=`{active: tab === '${name}'}` title=`{voc.${name}.heading}`) + svg.feather + use(xlink:href=`data/icons.svg#${(name in iconMap)? iconMap[name] : iconMap.default}`) + span='{voc.' + name + '.heading}' + main + each name in tabs + div(if=`{tab === '${name}'}`) + // This outputs a templated tag name. Magic! + #{name + '-settings'} + script. + this.namespace = 'settings'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.currentProject = global.currentProject; + this.currentProject.settings.fps = this.currentProject.settings.fps || 30; + + this.tab = 'authoring'; + this.openTab = tab => () => { + this.tab = tab; + }; \ No newline at end of file diff --git a/src/riotTags/project-settings/rendering-settings.tag b/src/riotTags/project-settings/rendering-settings.tag new file mode 100644 index 000000000..7483636b8 --- /dev/null +++ b/src/riotTags/project-settings/rendering-settings.tag @@ -0,0 +1,31 @@ +rendering-settings + h1 {voc.heading} + fieldset + label.block + b {voc.maxFPS} + br + input.short(type="number" min="1" value="{renderSettings.maxFPS || 60}" onchange="{wire('this.currentProject.settings.maxFPS')}") + fieldset + label.block.checkbox + input(type="checkbox" value="{renderSettings.pixelatedrender}" checked="{renderSettings.pixelatedrender}" onchange="{wire('this.renderSettings.pixelatedrender')}") + span {voc.pixelatedrender} + label.block.checkbox + input(type="checkbox" value="{renderSettings.highDensity}" checked="{renderSettings.highDensity}" onchange="{wire('this.renderSettings.highDensity')}") + span {voc.highDensity} + label.block.checkbox + input(type="checkbox" value="{renderSettings.usePixiLegacy}" checked="{renderSettings.usePixiLegacy}" onchange="{wire('this.renderSettings.usePixiLegacy')}") + span {voc.usePixiLegacy} + h2 {voc.desktopBuilds} + fieldset + b {voc.launchMode} + each key in ['maximized', 'fullscreen', 'windowed'] + label.checkbox + input(type="radio" value=key checked=`{renderSettings.desktopMode === '${key}'}` onchange="{wire('this.renderSettings.desktopMode')}") + span=`{voc.launchModes.${key}}` + + script. + this.namespace = 'settings.rendering'; + this.mixin(window.riotVoc); + this.mixin(window.riotWired); + this.currentProject = global.currentProject; + this.renderSettings = this.currentProject.settings.rendering; \ No newline at end of file diff --git a/src/riotTags/script-editor.tag b/src/riotTags/project-settings/script-editor.tag similarity index 99% rename from src/riotTags/script-editor.tag rename to src/riotTags/project-settings/script-editor.tag index 6a634bb84..0ed199bf3 100644 --- a/src/riotTags/script-editor.tag +++ b/src/riotTags/project-settings/script-editor.tag @@ -1,4 +1,4 @@ -script-editor.view.panel +script-editor .flexfix.tall div.flexfix-header b {voc.name} diff --git a/src/riotTags/scripts-panel.tag b/src/riotTags/project-settings/scripts-setttings.tag similarity index 96% rename from src/riotTags/scripts-panel.tag rename to src/riotTags/project-settings/scripts-setttings.tag index 26618e550..b129710d4 100644 --- a/src/riotTags/scripts-panel.tag +++ b/src/riotTags/project-settings/scripts-setttings.tag @@ -1,6 +1,6 @@ -scripts-panel - h1.flexfix-header {voc.header} - ul.menu.flexfix-body +scripts-settings + h1 {voc.heading} + ul.menu li(each="{script, index in global.currentProject.scripts}" onclick="{selectScript}") code {script.name} div.toright.scripts-panel-aDeleteButton(onclick="{deleteScript}" title="{voc.deleteScript}") @@ -13,7 +13,7 @@ scripts-panel div.toright(onclick="{moveUp}" title="{voc.moveUp}" style="opacity: {index === 0? 0 : 1};") svg.feather.dim use(xlink:href="data/icons.svg#arrow-up") - button.flexfix-footer(onclick="{addNewScript}") + button(onclick="{addNewScript}") svg.feather use(xlink:href="data/icons.svg#plus") span {voc.addNew} diff --git a/src/riotTags/project-settings/structure-editor.tag b/src/riotTags/project-settings/structure-editor.tag new file mode 100644 index 000000000..e69de29bb diff --git a/src/riotTags/settings-panel.tag b/src/riotTags/settings-panel.tag deleted file mode 100644 index ad96a6452..000000000 --- a/src/riotTags/settings-panel.tag +++ /dev/null @@ -1,95 +0,0 @@ -settings-panel.panel.view - .tall.fifty.npl.npt.npb - h1 {voc.settings} - fieldset - h2 {voc.authoring} - b {voc.title} - br - input#gametitle(type="text" value="{global.currentProject.settings.title}" onchange="{changeTitle}") - br - b {voc.author} - br - input#gameauthor(type="text" value="{global.currentProject.settings.author}" onchange="{wire('this.currentProject.settings.author')}") - br - b {voc.site} - br - input#gamesite(type="text" value="{global.currentProject.settings.site}" onchange="{wire('this.currentProject.settings.site')}") - br - b {voc.version} - br - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[0]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.0')}") - | . - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[1]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.1')}") - | . - input(type="number" style="width: 1.5rem;" value="{global.currentProject.settings.version[2]}" length="3" min="0" onchange="{wire('this.currentProject.settings.version.2')}") - | {voc.versionpostfix} - input(type="text" style="width: 3rem;" value="{global.currentProject.settings.versionPostfix}" length="5" onchange="{wire('this.currentProject.settings.versionPostfix')}") - fieldset - h2 {voc.actions} - button.nml(onclick="{openActionsEditor}") - svg.feather - use(xlink:href="data/icons.svg#airplay") - span {voc.editActions} - fieldset - h2 {voc.branding.heading} - .block - b - span {voc.branding.icon} - hover-hint(text="{voc.branding.iconNotice}") - br - texture-input(val="{global.currentProject.settings.branding.icon || -1}" showempty="yep" onselected="{updateGameIcon}") - .spacer - .block - b - span {voc.branding.accent} - hover-hint(text="{voc.branding.accentNotice}") - color-input(onchange="{wire('global.currentProject.settings.branding.accent', true)}" color="{global.currentProject.settings.branding.accent}") - .spacer - .block.checkbox - input(type="checkbox" value="{global.currentProject.settings.branding.invertPreloaderScheme}" checked="{global.currentProject.settings.branding.invertPreloaderScheme}" onchange="{wire('this.currentProject.settings.branding.invertPreloaderScheme')}") - span {voc.branding.invertPreloaderScheme} - fieldset - h2 {voc.renderoptions} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.pixelatedrender}" checked="{global.currentProject.settings.pixelatedrender}" onchange="{wire('this.currentProject.settings.pixelatedrender')}") - span {voc.pixelatedrender} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.highDensity}" checked="{global.currentProject.settings.highDensity}" onchange="{wire('this.currentProject.settings.highDensity')}") - span {voc.highDensity} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.usePixiLegacy}" checked="{global.currentProject.settings.usePixiLegacy}" onchange="{wire('this.currentProject.settings.usePixiLegacy')}") - span {voc.usePixiLegacy} - label.block - span {voc.maxFPS} - | - input.short(type="number" min="1" value="{global.currentProject.settings.maxFPS || 60}" onchange="{wire('this.currentProject.settings.maxFPS')}") - fieldset - h2 {voc.exportparams} - label.block.checkbox(style="margin-right: 2.5rem;") - input(type="checkbox" value="{global.currentProject.settings.minifyhtmlcss}" checked="{global.currentProject.settings.minifyhtmlcss}" onchange="{wire('this.currentProject.settings.minifyhtmlcss')}") - span {voc.minifyhtmlcss} - label.block.checkbox - input(type="checkbox" value="{global.currentProject.settings.minifyjs}" checked="{global.currentProject.settings.minifyjs}" onchange="{wire('this.currentProject.settings.minifyjs')}") - span {voc.minifyjs} - - scripts-panel.tall.fifty.flexfix.npr.npt.npb - actions-editor(if="{editingActions}") - script. - this.namespace = 'settings'; - this.mixin(window.riotVoc); - this.mixin(window.riotWired); - this.currentProject = global.currentProject; - this.currentProject.settings.fps = this.currentProject.settings.fps || 30; - - this.changeTitle = e => { - global.currentProject.settings.title = e.target.value.trim(); - if (global.currentProject.settings.title) { - document.title = global.currentProject.settings.title + ' — ct.js'; - } - }; - this.updateGameIcon = tex => { - global.currentProject.settings.branding.icon = tex.uid; - }; - this.openActionsEditor = () => { - this.editingActions = true; - }; diff --git a/src/riotTags/shared/texture-input.tag b/src/riotTags/shared/texture-input.tag index 9c12bfcdb..d156f0dc3 100644 --- a/src/riotTags/shared/texture-input.tag +++ b/src/riotTags/shared/texture-input.tag @@ -5,6 +5,8 @@ If set, allows to pick an empty texture. @attribute val (texture's uid or -1) Current input's value + @attribute header (string) + Passed to the texture selector, showing a header in the top-left corner. @attribute onselected (riot function) A callback that is called when a texture is selected. Passes the texture object and its ID as two arguments. @@ -18,7 +20,9 @@ texture-input if="{selectingTexture}" showempty="{opts.showempty}" onselected="{onSelected}" - oncancelled="{onCancelled}") + oncancelled="{onCancelled}" + header="{opts.header}" + ) script. this.namespace = 'common'; this.mixin(window.riotVoc); diff --git a/src/riotTags/shared/texture-selector.tag b/src/riotTags/shared/texture-selector.tag index 94c20f4b5..1423ba4e0 100644 --- a/src/riotTags/shared/texture-selector.tag +++ b/src/riotTags/shared/texture-selector.tag @@ -4,6 +4,8 @@ @attribute showempty (any string or empty) If set, allows users to pick an empty texture (to reset texture). + @attribute header (any string or empty) + An optional header shown in the top-left corner @attribute onselected (riot function) A two-fold function (texture => e => {…}). Calls the funtion with the selected ct texture as the only argument in the first function, and MouseEvent in the second. @@ -13,6 +15,7 @@ texture-selector.panel.view .flexfix.tall .flexfix-header + h1(if="{header}") {header} .toright b {vocGlob.sort} button.inline.square(onclick="{switchSort('date')}" class="{selected: sort === 'date' && !searchResults}") diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index d0b30101c..6b3fbb3f5 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -98,10 +98,15 @@ border-radius br border-top 1px solid bd border-left 1px solid bd + &.vertical + border 0 + border-radius 0 padding 0 margin 0 display flex flex-direction row + &.vertical + display block li text-align center cursor pointer @@ -111,7 +116,6 @@ list-style none padding 0.25em margin 0 - display inline-block {trans} box-shadow 0 0 white inset if (themeDark) @@ -135,6 +139,17 @@ &:last-child border-top-right-radius inherit border-bottom-right-radius inherit + &.vertical li + text-align initial + border-right 0 + padding 0.25rem 1rem + overflow hidden + text-overflow ellipsis + white-space nowrap + &:hover, &.active + box-shadow -2px 0 accent1 inset + &:active + box-shadow -2px 0 acttext inset // resource cards diff --git a/src/styl/tags/actions-editor.styl b/src/styl/tags/actions-editor.styl deleted file mode 100644 index 05ee0cc74..000000000 --- a/src/styl/tags/actions-editor.styl +++ /dev/null @@ -1,5 +0,0 @@ -actions-editor - z-index 20 - -.anActionMethod - @extends .menu li \ No newline at end of file diff --git a/src/styl/tags/scripts-panel.styl b/src/styl/tags/scripts-panel.styl deleted file mode 100644 index b71452a4e..000000000 --- a/src/styl/tags/scripts-panel.styl +++ /dev/null @@ -1,15 +0,0 @@ -scripts-panel - li - .dim - color bd - {trans} - .scripts-panel-aDeleteButton - margin-left 1rem - &:hover - svg - color act - .scripts-panel-aDeleteButton svg - color red - &:active - svg - color white \ No newline at end of file diff --git a/src/styl/tags/settings-panel.styl b/src/styl/tags/settings-panel.styl deleted file mode 100644 index 7e2b868ad..000000000 --- a/src/styl/tags/settings-panel.styl +++ /dev/null @@ -1,5 +0,0 @@ -settings-panel - display block - padding 1.5em 2em - h1 - margin-bottom 1rem \ No newline at end of file diff --git a/src/styl/tags/method-selector.styl b/src/styl/tags/settings/actions-input-selector.styl similarity index 70% rename from src/styl/tags/method-selector.styl rename to src/styl/tags/settings/actions-input-selector.styl index 91d93c4fe..a270958f7 100644 --- a/src/styl/tags/method-selector.styl +++ b/src/styl/tags/settings/actions-input-selector.styl @@ -1,4 +1,4 @@ -method-selector +actions-input-selector position fixed z-index 10 width 35rem @@ -9,6 +9,4 @@ method-selector .panel padding 1rem box-shadow 0 1rem 2rem rgba(black, 0.35) - height 100% - .flexfix-header - padding-bottom 1rem \ No newline at end of file + height 100% \ No newline at end of file diff --git a/src/styl/tags/settings/actions-settings.styl b/src/styl/tags/settings/actions-settings.styl new file mode 100644 index 000000000..873305b58 --- /dev/null +++ b/src/styl/tags/settings/actions-settings.styl @@ -0,0 +1,7 @@ +.anActionMethod + @extends .menu li +actions-settings + h1 + margin-bottom 1rem + li + list-style none \ No newline at end of file diff --git a/src/styl/tags/settings/project-settings.styl b/src/styl/tags/settings/project-settings.styl new file mode 100644 index 000000000..23f8f0900 --- /dev/null +++ b/src/styl/tags/settings/project-settings.styl @@ -0,0 +1,20 @@ +project-settings + display flex + flex-flow row nowrap + h1 + margin 0.5rem 0 + aside, main + height 100% + overflow auto + aside + flex 0 0 auto + width 18rem + .tabs li + padding-top 0.35rem !important + padding-bottom @padding-top + main + flex 1 1 auto + padding 0.5rem 1.5rem + border-width 0 0 0 1px + border-radius 0 + @extends .panel \ No newline at end of file diff --git a/src/styl/tags/script-editor.styl b/src/styl/tags/settings/script-editor.styl similarity index 100% rename from src/styl/tags/script-editor.styl rename to src/styl/tags/settings/script-editor.styl diff --git a/src/styl/tags/settings/scripts-panel.styl b/src/styl/tags/settings/scripts-panel.styl new file mode 100644 index 000000000..3ec832288 --- /dev/null +++ b/src/styl/tags/settings/scripts-panel.styl @@ -0,0 +1,22 @@ +scripts-settings + .menu + margin-bottom 1.5rem + li + padding-left 0 + padding-right 0 + &:hover + padding-left 0.5rem + padding-right 0 + .dim + color bd + {trans} + .scripts-settings-aDeleteButton + margin-left 1rem + &:hover + svg + color act + .scripts-settings-aDeleteButton svg + color red + &:active + svg + color white \ No newline at end of file From 6d1ca6b4350dd99008eac7cd58a0e2aaead6753c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 5 Jun 2020 21:02:39 +1200 Subject: [PATCH 04/86] :zap: Expose lintJS, lintTags, lintStylus, lintI18n in gulp CLI --- gulpfile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index b5579178f..e554b73e3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -528,7 +528,10 @@ const launchDevMode = done => { }; const defaultTask = gulp.series(build, launchDevMode); - +exports.lintJS = lintJS; +exports.lintTags = lintTags; +exports.lintStylus = lintStylus; +exports.lintI18n = lintI18n; exports.lint = lint; exports.packages = packages; exports.patronsCache = patronsCache; From 6c55eb2fd2a5561e3c031206f0fafa2baea988cd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 5 Jun 2020 21:04:31 +1200 Subject: [PATCH 05/86] :bug: Remove a gap above custom header at texture-selector --- src/riotTags/shared/texture-selector.tag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/riotTags/shared/texture-selector.tag b/src/riotTags/shared/texture-selector.tag index 1423ba4e0..300bbd880 100644 --- a/src/riotTags/shared/texture-selector.tag +++ b/src/riotTags/shared/texture-selector.tag @@ -15,7 +15,8 @@ texture-selector.panel.view .flexfix.tall .flexfix-header - h1(if="{header}") {header} + .toleft + h1(if="{opts.header}") {opts.header} .toright b {vocGlob.sort} button.inline.square(onclick="{switchSort('date')}" class="{selected: sort === 'date' && !searchResults}") From aaa6f4e56e6f1dfdc493018ab41acff81d4b553e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 5 Jun 2020 21:06:12 +1200 Subject: [PATCH 06/86] :fire: Remove a button in the nav that toggles fullscreen view --- src/riotTags/main-menu.tag | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 0c6f8c6c3..bc27bb740 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -1,10 +1,5 @@ main-menu.flexcol nav.nogrow.flexrow(if="{global.currentProject}") - ul#fullscreen.nav - li.nbr(onclick="{toggleFullscreen}" title="{voc.min} (F11)") - svg.feather - use(xlink:href="data/icons.svg#{fullscreen? 'minimize-2' : 'maximize-2'}" data-hotkey="F11") - ul#app.nav.tabs li.it30#ctlogo(onclick="{ctClick}" title="{voc.ctIDE}") svg.feather.nmr @@ -90,16 +85,6 @@ main-menu.flexcol window.signals.trigger(`${tab}Focus`); }; - this.fullscreen = false; - this.toggleFullscreen = function toggleFullscreen() { - this.fullscreen = !this.fullscreen; - if (this.fullscreen) { - nw.Window.get().enterFullscreen(); - } else { - nw.Window.get().leaveFullscreen(); - } - }; - const languageSubmenu = { items: [], columns: 2 From 3c397ed69e02dfde60b15e3dee5f98a63fb52099 Mon Sep 17 00:00:00 2001 From: araujo921 <53831977+leedigital@users.noreply.github.com> Date: Fri, 12 Jun 2020 02:30:47 -0300 Subject: [PATCH 07/86] Minor fixes to the debugger files (#197 by @leedigital) --- src/node_requires/exporter/index.js | 2 ++ src/riotTags/debugger/debugger-modal.tag | 10 ++++++++-- src/riotTags/debugger/debugger-screen.tag | 7 +++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index 33cefc8fe..79556759a 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -71,6 +71,8 @@ const makeWritableDir = async () => { }; // eslint-disable-next-line max-lines-per-function const exportCtProject = async (project, projdir) => { + currentProject = project; + const {languageJSON} = require('./../i18n'); const {settings} = project; await makeWritableDir(); diff --git a/src/riotTags/debugger/debugger-modal.tag b/src/riotTags/debugger/debugger-modal.tag index 4e15887ee..d0199d5c0 100644 --- a/src/riotTags/debugger/debugger-modal.tag +++ b/src/riotTags/debugger/debugger-modal.tag @@ -6,7 +6,13 @@ debugger-modal.view br code {address} script. - const port = window.gamePort; + let port = 0; + const passedParams = new URLSearchParams(window.location.search); + if (passedParams.has('link')) { + const link = passedParams.get('link'); + const url = new URL(link); + ({port} = url); + } this.interfaces = []; var os = require('os'); var interfaces = os.networkInterfaces(); @@ -56,4 +62,4 @@ debugger-modal.view }); } }, 0); - }); \ No newline at end of file + }); diff --git a/src/riotTags/debugger/debugger-screen.tag b/src/riotTags/debugger/debugger-screen.tag index 0debde4bc..d97330566 100644 --- a/src/riotTags/debugger/debugger-screen.tag +++ b/src/riotTags/debugger/debugger-screen.tag @@ -223,5 +223,8 @@ debugger-screen(class="{opts.class} {flexrow: verticalLayout, flexcol: !vertical this.showNetworkingModal = !this.showNetworkingModal; }; this.openExternal = () => { - nw.Shell.openExternal(window.gameLink); - }; \ No newline at end of file + const passedParams = new URLSearchParams(window.location.search); + if (passedParams.has('link')) { + nw.Shell.openExternal(passedParams.get('link')); + } + }; From abd31f4789ac251d958e1dafc5f38b728f7ba40a Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 17 Jun 2020 17:21:51 +1200 Subject: [PATCH 08/86] :bug: Fix ct.tween.add not working as expected for useUiDelta See #198 --- app/data/ct.libs/tween/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/data/ct.libs/tween/index.js b/app/data/ct.libs/tween/index.js index 88987dc38..0c704a8c1 100644 --- a/app/data/ct.libs/tween/index.js +++ b/app/data/ct.libs/tween/index.js @@ -22,8 +22,7 @@ ct.tween = { fields: options.fields || {}, curve: options.curve || ct.tween.ease, duration: options.duration || 1000, - useUiDelta: options.useUiDelta || false, - timer: new CtTimer('ct.tween', this.duration, this.useUiDelta) + timer: new CtTimer(this.duration, false, options.useUiDelta || false) }; var promise = new Promise((resolve, reject) => { tween.resolve = resolve; From d3772f88c7562abfb4833ac8adab0a063c0ab177 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 17 Jun 2020 17:22:10 +1200 Subject: [PATCH 09/86] :zap: Improve migration process to v1.3.2 --- src/js/migration/1.3.2.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/js/migration/1.3.2.js b/src/js/migration/1.3.2.js index 462a2f370..6463073c7 100644 --- a/src/js/migration/1.3.2.js +++ b/src/js/migration/1.3.2.js @@ -9,11 +9,11 @@ window.migrationProcess.push({ const s = project.settings; s.rendering = s.rendering || { - maxFPS: s.maxFPS, - pixelatedrender: s.pixelatedrender, - highDensity: s.highDensity, - usePixiLegacy: s.usePixiLegacy, - desktopMode: s.desktopMode + maxFPS: s.maxFPS || 60, + pixelatedrender: s.pixelatedrender || false, + highDensity: s.highDensity === (void 0) ? true : s.highDensity, + usePixiLegacy: s.usePixiLegacy === (void 0) ? true : s.usePixiLegacy, + desktopMode: s.desktopMode || 'maximized' }; delete s.maxFPS; delete s.fps; // A legacy config that was relevant prior to v1. From 90702dabdfa5f0d64120dbec45264ae9962486ff Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 17 Jun 2020 17:22:55 +1200 Subject: [PATCH 10/86] :bug: In the settings panel, fix rendering -> max FPS field --- src/riotTags/project-settings/rendering-settings.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riotTags/project-settings/rendering-settings.tag b/src/riotTags/project-settings/rendering-settings.tag index 7483636b8..8e44a33b8 100644 --- a/src/riotTags/project-settings/rendering-settings.tag +++ b/src/riotTags/project-settings/rendering-settings.tag @@ -4,7 +4,7 @@ rendering-settings label.block b {voc.maxFPS} br - input.short(type="number" min="1" value="{renderSettings.maxFPS || 60}" onchange="{wire('this.currentProject.settings.maxFPS')}") + input.short(type="number" min="1" value="{renderSettings.maxFPS || 60}" onchange="{wire('this.renderSettings.maxFPS')}") fieldset label.block.checkbox input(type="checkbox" value="{renderSettings.pixelatedrender}" checked="{renderSettings.pixelatedrender}" onchange="{wire('this.renderSettings.pixelatedrender')}") From e5417482d522de951651a52df60d4c4ee552ff2f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 22 Jun 2020 16:24:21 +1200 Subject: [PATCH 11/86] :construction: Spring Stream UI theme --- app/data/fonts/Comfortaa-Bold.ttf | Bin 0 -> 139988 bytes app/data/fonts/Comfortaa-Light.ttf | Bin 0 -> 140136 bytes app/data/fonts/Comfortaa-Medium.ttf | Bin 0 -> 140128 bytes app/data/fonts/Comfortaa-Regular.ttf | Bin 0 -> 140136 bytes app/data/fonts/Comfortaa-SemiBold.ttf | Bin 0 -> 140144 bytes app/data/i18n/English.json | 1 + app/data/i18n/Russian.json | 1 + app/package-lock.json | 8 +- package-lock.json | 9 +- src/js/codeEditorHelpers.js | 1 + src/node_requires/monaco-themes/spring.json | 215 ++++++++++++++++++++ src/pug/index.pug | 2 + src/riotTags/main-menu.tag | 8 +- src/styl/tags/project-selector.styl | 1 + src/styl/themeSpringStream.styl | 101 +++++++++ 15 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 app/data/fonts/Comfortaa-Bold.ttf create mode 100644 app/data/fonts/Comfortaa-Light.ttf create mode 100644 app/data/fonts/Comfortaa-Medium.ttf create mode 100644 app/data/fonts/Comfortaa-Regular.ttf create mode 100644 app/data/fonts/Comfortaa-SemiBold.ttf create mode 100644 src/node_requires/monaco-themes/spring.json create mode 100644 src/styl/themeSpringStream.styl diff --git a/app/data/fonts/Comfortaa-Bold.ttf b/app/data/fonts/Comfortaa-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4600581103f0d08d17bcf21e971034cf3ad5662f GIT binary patch literal 139988 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 z z@e*q!CZF9{Do10n)b`8q)&!AAXMPh__Or!iagLQCPO~ztENt)l#Re-|Ji@ypxmKPv z(VApUwx(E9v4tOrJ@ja-qQ_tporyhkHg?fDSVjx1xz;?apvPM$So5tytH@el6=S=9 z1IzSVSgOy%(w&X9{&np2Z;IEf606i&Xf3i9TVIW^h2 zJ~Om8;ozRyB6Dd`aaCnSi_FSJl@+B+qB5(>Di#$Luc#?)k(q1ut+R{Es)|=EU07ba zIx4%QvZkoGxU`}sGN-tR3)xjw7S%-Nn&?O78b|x?Sxzdlomgc%ch7cGk!@m6EYhQL z!@)Y2Itj=MC17}qTqmNrc0?V2*^v{?9a~RqjHF3(N@{A<#BivtNehaqT2E^9qb5&@ zoE*+^;^5pc+uSfEH`^TJp4noubK}YOjUy-5l$Dp1ww}@$n0>>v;gQo!hDA;bXF_sH zT9#`X(TF_CK6O-+Q|ZI3+=@k!M~9=4lAGgXQH~Rx94D)CoPy3VSxK}^fhVV?I{A|p z%37x)Wama4y|}Wf!p@}}CsDbf&>UYmP66d6w>^6C3W~jI#nSSk6*VS3smZx5j&_o+ zD-pG__0iSkMb(S#+F>eIYIfudJ7F`LBy5-y%M=r<zU0mEj7hS=qMAF zm5N@nEja9$CrlP9^M{qTs{jcLwx0?u=CCO4D^PTF&G+s^)R znp1|inC+x_wq4v2vvq@roNWp{a*myhIgQC^HK(Mkw5qhatU7YeqN<{mrL7AZPukg^ zmmXPQPC188?xZ=TkvB4Ot_e13Zn(Zi7KIZ^zUSE&W{q(6Db7C4*$>nE)q?#(!$E6HaDmZ^EN;rL`z>GNH&}g`!4QC@Qt{ptv!aQN@iBFEIrd zS!&`RSsE_Jl-w*Q8QD&BvYp#zJITnllaZP{EUGjdo*m;XC+1n9n2%^t>cp`0AUZH9 zPEH**DsqvzbL&Nou{4QJNllAd6b{|BSkuDdMn92d;e00_Mzty%QeIhHK5NEQJ098Q z{;-3I4?eO~nWsgW6O}TDAj(W#vGX`>L~BoD@OBi^MzmS7sH(KIqP(c0q^vly++=8E zc{s0Njcivrv5c&+PgOKIl|I}mr9xJQBMNIcIho^RQ;w6PIZj2$F*#0jo$N_TlwP_l$_V1(#a2-HL_Y){)jblBCG6#RW(W2 zFejENCRViXw8(0^s#Q13!PJyd5#^N?i>h5Unk#CW1Tx&I!zo#jE5bRHoMQ7QVLCaI z8`_VESkai`TqpnXoP5a*<$#m&+`P6cew^YI;vGt{!x<|KXRNR}W0jqVRgH;gwJOXR zt8C6#(|FRZX?epU*EHn}CvVc6vd9}1d7=q6>cntmwieD`7y;&&nQghLZ0QaMr#rct zo???}dWxM<=`?m-PU)j;u1>dEH96hRspNFK>%iVFm2?==_}8h{*VzQl-fj}^hg8Vr#DS6uT~_r`W`wo?>cMa(b#0k5uP=sZM-TL-BR|ryADBzs(xy zsZM-So%p5N3OqeE6t7VD&ix(DneMcO^yHAgP`sURlSB7$;+yP*pX}Vv?l_P?PWnLT+($U!k8+Mjg^r#3jB?U3%DHZo6MwramzTsH~=VO?yY;xN=Ep@ftfOcBe8qJ0_na-LvF$o0r);DYE+{$?0}qguN3( zr!SIj_f>FrVrKU$lGE+p27AYUu9G5LGvn^$l&ztY)AO8^=&X`$ngKPP=@PEAO6?w9V|FWrf6x)aY~j{kHgp6O0J)1COFJMl|*;*}nX zS15ev{^?Hq(w%q>3;7GhJCy#=eVq6@y|#3Rhtr2S_ZjAdlOBqnbH8Cu{2j_oAMW@Y z;iPATbDvS6atIwe=^5q3Ym{@{C?_6vcZ8bkTxWM@IL>OdB3!c_saef-LbkgL)Nm){ zY$wL{g9kM`R8yUju=OQ1*omQ|FVk&(in|jtTk}zKotW8Lgk$Hvw*KJQxu2~^sM(JH zJSY4-$A6xk1H&9{9F~&P#?;ufv?0~i$te+;<;xZqxw1-YiXtW!EnQk<>T?z~ zvux4QB5PLBiWal&pseYO%dBiLy}Hc4P3EX5bN`ykipuIX&K=D`OYUfdR{Dnac_)1P zzzNeFL>;}fbdlppdk<;8_Kv5}v3*l|AVnt83rJ7IBAw`nlG5^;q86o2l27D>rBBwF zB$w$VFEL3jH%Yd`$;wt0E7a;T`XVOXR@LH4=MJe6)h6*ZBz}dH_+=!#7;qR-spH#9 zhifNBOQA@1Y=_b$zj?JJxpnS&^EH(N9bx4k%tH3uz8tP$+^=Zt7| zVrf<7kmAav%c4rl7A>wBQc_X5wAI40m8C-}RxGWGs%99W!ieUIWT!PGXW8vFIm^_> z)Z`KAt$5-$K`d@lvzQL4EmybJVL5Ue6Pk0-s&sWRxm>cq^euQ!$c8(O>GBn2D~rlYD~e0ol<5qrE-kJpW2{A|G{x?yq^3F@3KA1lSyi)G_aKYPTNG7Q zRjyjGEXwSwbTPLwVoBwy3R@~KUFf(isH|CRyRBGOQqjh_w4`D|b*X);#lh^es)Avw+IlcYHeo@!@Na%$-D%X$1sbNW7M$s<~s zzAANcK`YZO)sntS~UdfUa7l}m~iRIVhp;c#qD zO>_FOX-+>jEhVcBJ>UhU<&~=%Jv*I4rbe|R;dBnuoK9hy(@9Hnx(;bhmmn=|cq{WP zF{G%xrgb=4LmWm-Pmi!$s@V=HE~+kV8*1vtskNQr;iNjHiJaU-9@Runb7UG)*uR!T z{5>YCD}i#wE*R@3%qQ^jjcd|EegXl$sIPsyj)K7*x3Vy6kP z{ai9f57v|)*alUkkbEK1iY-&OvF)t7vyD+PY~%UnUTO;n+dirfTPz@Kv4F50tR}IY zs;0BWhQW5Wn$32On!^@52HWG9ab9A{Sj4tWF#{xai=}KURVCZyYB}3g%=RF$TdZMw zvO1aVsp=fIdzmL+VgcC4_I>p}+Yi-;Y(HV-Uoz(ZCEKrAYe_PCPyDUcjNnVY#hJ>e z`aEkM+xgZ){f+|=e4iy4CD%&|>NT3PoiS}jv?)Q0t1@|2LtoY2Tt!Y0Ru)^q*CYe5 zU#XnDX|qJT@}imwO5cUMo{w3Ftm}idq_nC+bXi(dwM4`$U9xlu)&hM-|CI5e-vqI^ zE`qO6+8y`IDO1)DSzo(h6K7k3sKIS0jk`>^uME1R@t?FeY44!(8jq10_}|EXU*li< z`qlr)q)6_uzVXkIzQ<4gllnCIpODn-Kd7ql&)9zWpTz4Lp}&d$jl?)|&?V;HdfUKp zA9Lms9p)g;YnXqZK~+gYhueC(RqlE*Vr0ATTFhuYz1?>muIuz*r*EUbi!F((in~8< zckk`}YkFVPyS9(7-yQv4=|7|2d;O#P_vt^Qe@*}M`rq9D#r_|d|K6mn{9iO%Ep2t& zKj{B;|E~u`@}ERbSAq3F=fBSH2I8-Cs}J$hR}(kN_7!6zil=rZQ0j?T)Ad)39yiyv5qeX*YgeG1~}ymIOSYyDd&p|#D%cSR_r^MGdBSCO};C=OCfJJH_wwZctzVC_m`A+g6o4Pz!&M{W> z?b_aqo%wp~%eQEoF&5(o<-=HnwH4U5|GsMMyV!L3CixR(8LR3*$L2W=Yp1q#&JNo+ zwS}_;i)Oj8Vpd_lw5^xgc6kbR%5$(yZo?v}Esi&1X|(N(_l7Nsjn>3xjQ#Lc$7Z;X zucoxM@IzxMGR5rsz5cU0V}{tunMdOYru)%B(N5o4Ay~DzG&N4rUR%0o($7;4$z#c!6^-f;W-t!MosNW~!MOtXI?6 z9|ev;ZehTYxat`{3)K-y%OPou56F zCs?b>@y)v_YFW_7oHrv?H6f7`gf>#G3HBkxk?It%fjeCv9HMRjH-nwQb!r!Q82k-9 z0v-jsIrjv35`05hZWNCOcZgc>7}yOS2Ty<}!BgOA@Cw5xfNUfS185 z;8jouUIVX#H^5%-CU^_%1Mh%(@Gf`{ybnGAAAB^J7z4(FabP@{05U)($O73Q2jqf0FcC}wlfe`~`Xp(Qq(zbzNm>-~RK!oU09`;l z7zl`qA})$}DB_{Guj0Pcl7OQ9P)lk}?O27|YUC5yp9Iz-uVa4-wPzFionRMu82k-9 z0v-jFk)mv<89R8RsTZrG#esO>1__`y7z@UM@n8bT0GXf^ECh?dVo(M=UmHOSOZQ3CxNx#WUvmL0@j050rj3%UPmjh6Xyo&#d+X-Z~?dw zYyn$2cNaa8yTLu+Uf=`wf&0M&;6dOlHf#<;s z;6?Bf*aKb$uYgxU9e54A4&DHJ!JFVMun)Wg>cP9P6y1|^^rECh?dVo(M= zU%d>Y4)9lSJ-7kf2yOy5gImC@;I^Pw zc)?Dv3)~Lw0C$1A!9Cz!-~;!8`@sX?LGUPgycW=pfKdW4N&rR)z$gJ2B>ABe1O&+pmhgm z-2qy6fYu$Lbq8qO0a|r{R&DnQXR|K|dg%*#=>vM57OY!tfEF82)Fnk9OVPigPq>9X z;TF>;Bu=JZNLhyah9A>vzrz35L9f*UxSf8Y=fAS0-WF`2RW;D68faAww5kSLRRgW6 zj#gDitE!__)zPZzXjOHzsybR#9j&TPUO+o|J`=+l_zyw$XFef3)A=R5^n|5Lqndg%<3_2Stz4#WdD zNC3UTSTGKZ2NOUB$ONTeA>f&mUb>%Nx?gy}5>O77f(lRxmVxD<3h?AfFWpZs-A^yw zPcPk1FWpZso%h(mNnkBF8LR_5YtT#g(@Xc$OZU@D_lu3-r+evs+J^^y=|Nw5(3c+c zr3ZcKL0@{%mmc(`2Yu;5UwY7&9`vOLed$48deE01^rZ)V=|Nw5(3c+cr3ZcKL0@{% zmmc(`2Yu;5UwY7&9`vOLed$48deE01^rZ)V=|Nw5(3c+cr3ZcKL0@{%mmc(`2Yu;5 zUwY7&9`vOLed$48deE01^rZ)V=|Nw5(3c+cr3ZcKL0@{%mmc(`2Yu;5UwY7&9`vOL zed$48deE01^rZ)V=|Nw5(3c+cr3ZbLHByly&iP02i@zT=k1~A?V;!G zK@WS-!yfdohn}~Gp0|gdw}+m$hn}|w-Rwa(d(h1ubh8KD>_Ins(9Irt-X4119(1$^ z9qmCAc-0he6pVii``PU0f%(WhX_$6hNjufg)yrY_DrBB6(Bs=_;~skA>u9MSp0ho) zbq_6e3(wgeT5T;OGWF_Dsd;RX+y#qadiJ@f;${O?cfgZC|_vRg2%va@Hlt^JPDox zPlIQ`v%n9Y1J8pOz>DA|um`*hUIDLyI`A5J9lQbdf;YiiU>|q~)Pr}yd*FTW0r(K? z2Ooit!6zU9J_VnF&%qbq@8C=D75Ex_0~)|Tz_;L^;5+a=_yPP29AMsc0VGg>Z+>Bb z?J&T085i`)zMvoI4+elEz(6nvB;jW;7y^cZWRL<gAu+8XAdFLp@w_yl*# zNPb3qJ_lcbzXQq#M%fOdY==>{!zf-D#S5c&<@cQb0UQ89^eyG0Bzww4x!AX09|a?{ z25mrF+}g4406HOe;d~5xJ)d?0``&=^Q=b1O8*NuZ*;76+(smeWJB+j)M)JZ) zUKnXRjI>>4;U^oA?+z<%H>_lj1=PYoe%Qwk`)r4Kw!k{uVVUi)%oeqU>#h$L!Zd|2 zO(9IPUF`x7gTH}Cz@uO{=bivh0-I^J8>XQhgjwb*dN$+}OrxJP4Ab!aPA_`kap1pY zL{N|XJsR}EC_Wg)2c!646d#P@gHe1iiVsHd!6-f$#RsGKU=$yW;)79qFp3XG@xdrQ z7{v#p_+S(tjN*e)d@za+M)AQYJ{ZLZqxfJHAB^IIQG76p4@U99C_Wg)2c!646d#P@ zgHe1iiVsHd!6-f$#RsGKU=$yW;)79qFp3XG@xdrQ7{v#p_+S(tjN*e)d@za+M)AQY zJ{ZLZqxfJHAB^IIQG76p4@S|9;e#=JFoqAt@WB{97{do+_+ShljNyYZd@zO&#_+)y zJ{ZFXWB6bUAB^FHF?=wF561Ap7(N)o2V?kP3?Gc)gE4$Ch7ZQ@!5BUm!v|ydXuXZ2 zZ%s$uemvr)Td9v$>Z6tVXr;O(>Q<;*pl)Tlh3S^0Tak}elqs7=G zX}Y!0cXgh(eP%3t4d+h+Ytha2h?(wLA8xeFN2{o#f9|7I*dt}%82gMpLe_~p)X^U6 zO|PF`@ULorT30AD(x>~_-~690CqMpw(&hh?y83^UK0b{8HTqWT+U9yTq+_*y)w)&d z(4W(vht`{0Xs28L$Mh!e^YA81OEb=N9Gr0qoUsiSdJ4{X2D}8r?18~gMpyaKRep4p zA6?}~SNYLZesq-|UFAnt`O#H=bd?`n?U>@}q)^pSAz)EgL2zeWa zJFHWz=)vkT1GtlZ!($zHTF0H%u|`b+_X}O$+T2TzQ!WE~oKjm6e~4R8xzuEyQIDaoIx7ZNZm*qS=D4E%b_x zqJ_++g=nb(se$x_dU#k>SVMz%DHyle!Rj_q>J&x~Iv5x2Tf1@vckp3D)w!nMpCPC5 zxe|F52=xfLk~{i%hN@$3>t5LW;y^rbg9OkUj0NMscrXEEfJ}guT41FXSg8e8YJrtn zFm}XvYCYqr^^B+1GoD({cxpZ4sr8Jf)-#@3FBnG>Sg9FLtru9S#VW8GtN|y2lfYVV zGFS)D8jPpbGoD({cxpZ4sr6zb*a~jreGM;Qq!??;4y-9Vu%_&QSsKtM4d{~w^hpEy zq(M9Zo&-;Ur@=GeS>OlHf#<;s;6?Bf*aKb$uYgxU9e54A4&DHJ!JFVMun)Wg>cP9< zJ@7vG0DK7cgO9++;1du4pMuZ8=im$Qckm^^l7c>IK%X?APa4oC4d{~w^hpEyqyc@> zfIewJpERIP8qg;V=#vKYNdx+%0e#YdK50OoG@wr!0lTb4n}~H zU=$b)#(=S492gHKfDDicvOqS-0l6R#Oazm_WH1F_UzXUF8TZt09n>>&Q7?JNijj+Y z`6j3b?*eRll6RfZKMm-g2J}w@`lkW?(}4bIK>swLe;Uv~4d|Z+^iKo&rvd%bfc|Mf z|1_X~8qh!5x~1Pq)6YWnv;zH3TD{8WSTn(K>?`oI46FpJz#4E8*aR-1#ciXN?ByED zl;@6 z{cW4HVf%dt{jP(4&!Oo%4zvQUs3lhBy#^7%RX)<9)3J{Gtm8iG%zaEMc{34r(ZZy@ zsb!`KA7f?V*_3efSg#HXUCoL~$H9S>99IF&!!{4;XGOz9u!4_eMX%YEpYAn#+r7yRIQy{$)UH2F39Y;tTQ7;9>7@*ozN&vUv;+ z>FGB)2C@9w0hsjH1$m?A=hE5yhE4qkx1e4BtzYQ=PAqh-^fV3NPFJIY~1&Cfr>Ex$MdKjX1^9iRvCAN^4R zesbDp*EbUzN?F$dm_lGc}@kjF#Ieb`$K6`MS5ACO^D|y{KhaFb^aXW2#-5)0h8R>7#r$6pKjUgYLXFA;> zMr!o)pO$_B(;U=m`t9*MxPAxM@&65s5Zv?&QqU~Y%}c$hwr}S3Hys|FQah;T_r^8u zH{Ik{T^#hliP+tBzXW!2@*h^xdSM-BYK#^_Lo{k>u0SjOwqE_OifrRWziUn%q9hvc zWF)N@e)c!uXMOyn2XZw0v|@W|tcgF#(Z6?V)GQR%4#dcje?E5(m8|CJY3?T!dZW92 z#X;^)3PWx|o}vDmevK{W_m1^XuRjju%dZdNw_f>2m&b37pMCvL#q!|Vr~Uqv&rm$Vwc_8b zhq_klYn|t+W>?#F{-A9$AEEm<#;|Ejc~UUrMYgY|GXDQV&rFRm{I{e#jPG*}d(E#H z%{y#(2cP_%V{o|MMsUc%xBma*sQFV8b?)C73;4bBplQ1}bl88KwmbCwezEKCToZrs zeSfXT!}KFzqz1HgXcV$p4^WTs{MwuTd)NJB44OUJHwpEBz7&2*3~WD5qW!yyza$2K zEGvhTV?U8PV?uvWtqJylvqpj%eOoQrKj*HT#kEgxB!62D`*1w|!8>Oh@OQRr931Ho zZ#EwRu-$*WJ^1S1<0uq^=3&`f@t=r+zH^u{nnxp4JE$YD8>0~HO?~;1$3MWq?swX8 z`(LZSnxs3#@1cE@Q-8Slzp=-Aa6bH841bS$b@*EH<38ITUM#=;28Zcy{QH43a`=;c z>O6@R{`;Xey+W7r!EEA$t4BG0^7*c52>AQS(T9Wm9q#Ct-s^B-{IYXDSFcIWAEd#5 zS^TuG<}qk~{L4f7Px?H38UFFQ@!!j(!_`_lq-JeTv67gcX;biS&=&SvndR<6=DFL? zjCUW)mSP{P)g2*%%%xet3U|k`j@txQZ#$bg?at+2CeP>Z(Q|3CzS~y*<;m{rU^WJR`*ShtKhu~@6@CiyKZlzHU=)k@yU%44nN-&9-GRz9NV z+mw$oOI}x5tGcW1@^QWLn0$hD$KvEutUTtHPqXIO0Qn5_Z4Q*rvF6xF`8=zOWy&{I zw#t?7Gq>JExt|rqrpb?(U+*aSsh(p`ex_FwlV7NORVcq??XXh$J@f1>A_i5eh8aIs z@>i@8b`o>+oyXtBOnX~cCv7XU?X@)X?X_XXy_;Dd?KW2SYR{@(ce6g)y{zKZl{LI{ z%%0)jm({zTBYrRN?{8+{8^HQquM$_ig4ZBDdnYr5GJ7X$q}B6JF|+ceGB4j}td{l# z|FO)=_YLc${eyo7v-7o3nO2+?r?T`seJWe8e5G=k8&l7**n?R!^_pD~qNn-yVy4BG z%(a-v`9Az3n02u)azEB-j9~u7F|0~BmN&>+n%NgynfVvnFazW9tN~Wa9EwVu#y`T$ zrPzyEEjHqErZ|Vy!Oo>_b)s%<#bq0-W_4$7#cNm}>jvggY{fUfcQE_so&5VTAJ>Dd z{`HV}gn1tyHL-k?HLhB)y4Bmv^tq1}?%FcvtDg1sL;e=^bU*UPthCrd9N=#;KUt8K z5`~NqJy@-=B{P8bkg=jSt1ZTf&dd_qM|23!56g_JtgJYh<;@R*=0> z-YB}$mTtlQR#qPDNBg-~bYiARA7}5A_u>8^v(vVsMLmp6i^2~rO0+X`)b`XfNfQ3^ z@_GETo}g&WN`kNA@)|1%M#?wjUgGnne3NV6l5gSuwtSmd@%AY$AG2nllAo|@U^^Kw zv)q0vKP7*@WK~=j>jr*9jy1>za^PF}9WJaINDd#6LEi49pn3O`WixpLlOHmF(Ux}M z!iDt%dAGBLYJq!8))9B_Rg2zcE?tW? z1!M6;+a?6sHevQt{Y7V16trj59mrKlDhYR17R2XJHI%z2t7P5?Pf;mcm8w#4XRSfp zS#6LvMMtO++-0O1$yrt%L=$S)#kM(wSyBMUoOr*eN_u}GHKG9#@r|#pcgZtI}$PcIokRMbJ zB0r=aLVj31iqGB5mlF;1JtdMg_pych#9+;RY+1cf4AtC+PqfiWJS8Vvw2HR9EB&U>NEdGt&0vNHd@^3>XLNorY{Po#wc< zO*k$Jj=L3x)$G>VuvD0U**F=%5YFC!$Ixfpf?D!k$u|zh<92$yz`m-yWyR0$N|kb9S!3|7{;*- z<5-4q;2RjnGK@3GFpe~gV;RP2X&6V(%-vFSWW~}pygjbD#zHH%L)MHF!OEqbaM8Te z!SIgMYnZ|dns;Iy-qDQH(V59x^G-`MpSQ)Drir+-swp9`swrD~8E}N=pq7SxEW1enn!f=f=T+>oz!7`TNmzIWKx*2BaW;i9qa7w(=Y|_oJNwQ&+c*7=x z4Vw%xY?5HuB;N2yl3|c;FvtShKqG?;H4Nf543c2x6z|P?s7q+E<;-=`*~~BA*UT^8 zkJV9^!3%np@fgE0!wky|Vy@v8_}BA}cUG&_YPd|#K5priQ*l|V*1|`6Ci1SVp}G#2 z^=duxsp?c1VS_rI(9Te2;<8C?;_O-KEZjG%&2atM>TG!J9Of_YXZS0FHCE4uF*S=# zFf2CSuvj{4uKo$e)Lb^kaM@_XWvPbCh8r&Hq*q#nJvF0sGK@B=36~{?xoJ0NSyz>< zW~hONp#~aOI>IoKGE5{56Dh+)ZCO?I9ZFO4kuprA4DUo5mT7HRrj^YyhF=n`4y<_^ zVY3WtllB%pSe>*Fy)qZQaY-*rvI=T%TJ_Cp5-g+VQ=jV0r{2TNryhfACcf<6`*G~` zJo)p?Jo$xYp8QkbrY&N0n;!~K1{0bSbdWL+~W4@G`s@Lfm^5rHyLq3ec4EZpM?$vKGz4|}tmXAMX$d{Ly{`}>p zKfj%pACQ06Gvv$r^$hv)LE37he3Ew9ioUn*vA;-r>n>l?txLY9+m(D>wopLVX`gkZvs7wN zMoY=mmO^yJ`r=4Ggrw-Ckkl2o=#koAvY?-0c6%Z$T@seIgeBaf?>E<88FsraEZrEEydf#}y^s`5y6k)C7}%l2z7=wd)+uiu z1AoY^Nu2kD+@fD6|J*t^Os)}cLfyxaE$LjdC7oABGWqwV@uzdmxb@Vf8Ux&oQuKka z8)Xz9h^G#8bA=@=n2uYAuoNAVdXlfs*`6V(C!uufZ9*9ka_dIBar~u+rO_ctU*Vk1 z2uVGOrJdexc_Ar|{EH)p*&=nDrllBHw|qCT?RG-986m0BO-tQ}>$8n+aUXO27y8xnZ}$uym-94}ZnF@U;uWQh8Xa3QLV4H^!y$+KWibcAXnN zVTE3_LyoTrOV@{`Tf@>FAt|nbQbaqEGaW#*xz-n!9u7(IIUz~=>%QCgdnV-8{l&0U z7nW$rc6f34i^B)ky%&~rTeaQ32uX2sDbMwBq$Fl#Sjq@XCxj*3V%BPZ-HE+@MR(F* zOFdLbirE^LriCRq$GHb?b|^73!fyFtsqt8#cT>ojrYAQ%vlF3`oBn7kz~jtGmL|{YY_feW-inWP;8Nq4U1y z0WHO3geA(zaibpCQjbT%(i0)6*YmWLSHRvzsfR!8=0RH7Yb`Mkgrz-U>GiO*FD!i! zmcm!`3WuVl9$%X9z76{$=Jq{eTp>xP1)2NCM1|ZMLy74ScB6LoqF&-JIxO`JO9OE4 zZO&?Gh?aWQ_u8M3&;GN6rI_?)ZuqvDrWe=7mg{R{CxxZLkfhJXj5cQ%hTP)j#-A9! zK7M2TIWa54Qbt&s7MAkD5^gbT%@s4kZuw#9gs@Z+lHz8Cq!>?FS{9Pxh=CK{hLBs# zrjVr1Hjkfn`{^rUYQtBY7nZh$rOQH6Tw};r#oQdTE9Rb<2V-`GrN&U63SALDh4wo= z{@D2A;x350C|<>FkGm#@`WW+>&eIrbrz7nTOZx0ybGAP07CzexuIokla^~x>L`$*V z^x0T_Qp83c& z;=+;%2*;3Ec*oKhQ*n`(kG5QzpZtP<=_B+j!(p}~}>}dD@ zVt+6fuxXSUdpA8R^x2+!dcNLMh5R-4I~&8(v5#MG{Qkebu01%)>WZIx?-m1sP{P~Y zWJ_N@D5F*xw3e2D88kqcl_+Ya2@j*i2u>1}!PfD`78NlvNFpeNpowp#gW}NE0dy3L zu`?E`;;1;b{-aZ^13p?c{r!I5?)R;>lg-?-XU{qJJnrLs_sgE$T?9<`OzBZEHUT=a z-d_FbZ7JoY%`kXE&Xszdosr2dzX^P{}^-k#@N0Xdxe-r{BT4|zn}gv z!oD$npI{f9PCBpC(Rru>yGN8;(w`SGa--{X#URM)tddE^*pL`I67_)AXF!|6SKP_K z=Y9i2E-BE)L_IPSGwC9hWNKc)qDPX8lXF;Ra?yM4(CATu{Sw@2QY&^uyowc>*%1a# z4s8KnmFW8wXiG>-uIHJn@JgqQ%uT>ni@P?nKC>~?mFX$*^igsk@%z}?u97i$w!nQp zu@+I#m-{c|Bpbg!v7gcvf0R*ERC0KWB>UV(q!hNFL1-LrZA$*hIiG=-Gz{%k~z@_0U zQo?6~`vKRd9spb%cAN80$<|T9o&94#UoFYiVHk1xV+R~9?vl(Cvi?t5&!=epaaqr& zXg!~z^?Z09uth$!%X(fq^MTgeY1f@8{d*MS9&-JO(z#te?4bsqCdFQ+UF(#jiPA|c zpg_htX~mitsddv9=+FiTTee9tR!MV?nr*+LxSrN+tK#~rC|l&=Hd%j29`?!Nr;OL1 zAq{P;H?*A)BL!uzDZk6w8vi?7MbYpLrsCYAxQ?nOhwRCm4nj1P1dN zBm5VDvqL#4vRlP>65VY7H^7tWC;aEW$k7KoN-OXdQCJ7<+7Vi`jsX3jFs=@2wo9qm zwo)^=jI@KAZ6h_?YDxY>-j-{J-Y)B788yDE571pCm{WAuDY{*f&ndcL z`mi2&tu!Am|J&u^@v?rrVqBwjey(Ij%gSiU{8pZiCvI;k-nWErQ*@)kb)dg1%1dr8 ztkjAAC-th6{#&K9P7yUImb_xAla&T}SSRZv1-m4FKpL8bpC$Qf;VVU7ubDb5dRjWu z(z9L~#%T7>ll&N28N)T;couYSOLzltzIH@j_)f{J5~W*|22tJ-To6I_MP_5)L!z81 zZRZPLCio-Nia39^@FT?C4&k+KJn*{<8UpZ*?x3_`j=aX-T`S4P0)!QRqwu4$k{7;- z5c2fl{0NG>Q#*R6D1CwrqBIC^S6tT;M`hCx^lfod0k5Do+pRqOjwBDt%1n)0ZGQ$b z?F- z{dsymP9vX_Oimg`N@k=eGc>jng?|tGjQNdLM#w9TjBAvhM>HA@(src(A;>gKlBZKm7gs4YBU7%|bevRzz5O)V^?;L7K z-Uqi&l>OrNaYb3D@LH{_HhI`a8EYgR-<1l_O;-&-Mmk5a&l3J3XW8YX^9jzf%PFG$ z;+~IcMdyAde0rfTHSk*+o!com&7(rh?NKa`i84<(@3ol$m>ahUpGq9wl0IAbqrz(> zgW3_$YqUpdMX42~M|h9$bNq{_--TSE`UF)8HWk0a<58#b(*Uo+j8JXE!>n+wm4{2h zB{o`>D9oqrg;?lXVDi|F?^?QmOc7s0B+S)rb}?|*F0yw*FP)v%HcIDf1I7>5SS`| zIQAUx;TrD|p!&j$G=wG_Zp=Q5&@M;g9Sr?jiFQMwt=zJ&ne@6gI}R58iyp6>43A&j>CO>=fK1xL5F2!FLH=xp3t$>k+0mNC-4)QE% zZ}Kc@U+ENffaf6ljHezu49Vik@^~GstdW=-%xI_rG-ZqwF~y44Zi$~~erEOT`F33M z!nS2tD=ElXe}7?p2E6_b*3xjS+3%u?{&Xvwe(^b3n>%Cr{H!gVkps-vOusm5-E(SZ zXYJWs?VPL~nosVh^K&&>4tfg;hBj;F{!~PYAtF~zA+q`22;FS%)8$1AS$N;=! z2jC4IfH$m!SH5szbDOm-UVPfb{7i6Y!@R~51jVLz_7GmhpI3W>}u3M2E2Ar<`X}+2MXFTo1xx=$aCi0G<&p1)PT5>BHG25)G1Ql*B?wERqDYhpFML*!v&A z6Wl$babNS;|WSeR8;87XY$2fn#1)MuK_-;L~^IP@Yj{ey9{(%B_ zTg0Ugz9*sy?=8Xa#7twJzcn7b&LP_Xn(>|!exc5tBgGYyjyd#qEk0jBNpF`r_KW=1 zC~y^OC2|zF(w>7v5*bIEBbo}6aO#+Z-{a-zCga4_<+)lx_42Z)og+wZ96sTY=;P`u z!wwya*-GQOdxBd7seY`cJM>)XZikosZmm|>oq(0pftvkNy%&<^K2jW!e^!|NwE1f< RyS$CU8WD;Fd^Q4){{qUkeSiP} literal 0 HcmV?d00001 diff --git a/app/data/fonts/Comfortaa-Light.ttf b/app/data/fonts/Comfortaa-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..216f8d18b6807e70533b9dcbf53c3ee78245ea4a GIT binary patch literal 140136 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 literal 0 HcmV?d00001 diff --git a/app/data/fonts/Comfortaa-Medium.ttf b/app/data/fonts/Comfortaa-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c1f37feffc83f0abe0273bba37cc22233333ecee GIT binary patch literal 140128 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 b{hsigKQoTRKgCsww-uE8_(OA>rSbm(B9L87 literal 0 HcmV?d00001 diff --git a/app/data/fonts/Comfortaa-Regular.ttf b/app/data/fonts/Comfortaa-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6dd1f3ae3e66aa6b9d7532d879aa8810ca6d76f7 GIT binary patch literal 140136 zcmcG%34CK$bw7UZd(zXsZH$Oa>Yf zwgeIgP?n@XSqcRbAdnyB2c#{5Qs_cR2uoUiq_hxdl9sZCSpI*{z3)BAOBVY1{PW4! z*45F|z2~0&oO>?gjIkhoSXjgChUuAmcHR3RV^wZaj7N+8c7ued_Ia z|1FGp9=z(6#}@bgy@vVCVLjTvoL#)(oaH{>0AADQ8(y(^_QW+~?|PW=8(sqhyyDy| zuemn!{JS4u{58L1tmeYG^C!;rY`pkG%(oWse-9jG8B^v|5A(4aR?lYHk>cgdZqsab z?UI{os!dZbGR~|HZav8y%w~7kPBYcT6^p8z#>bguFSA(KVJF9nt;vRfpR?h?zTVFE zhS}t7eO;n97V*~vY8)E#aj!>oRj2!RlcX47bJgF6s#^PkhtUkBsyApZR)|3Ycg550k!XgC$^Yi{ccdOX4A z+0ORqY;|kg=L-hh$|LlFr4D!Cb>buV*kY8THsVi9*pEB$AE=@-^e ze;t2kT_&h}v8HBeI@IWPEAL%8;dXy)eudxn)P4Cj z143OgA%@sJ>p@t)W?pXcp0`Je85CiPqbyx7P32}_wwfY%i1@sKZIxG0L`N`vJKPgu5tO;>~ z-M#wR0^(gJ#1-uJbs^Fw#5j9kJqYz*O^6)3ay5ilfk#5D*@H&&lld3G&a4SB#=a|` zy^2Q47%m#SvVO_~7z$AVrg2)MfK2!|eO3 zS1EA$q=aBuRX|9(=J&C{{feZ?`z`!Ie&33x{?dHv0rvjNxq)*7jHz!2=eDp8HdmbO zXjfE)Zw4`2?5gD?voo93Zac{og&!77%&bP@tK+dy!0&b1SPO4) zN?y)@nWZR$m`Vnu6icTAB;05Ok}IFKX@y)MHtdId;~#RwL;Qk28ugdn|1|x2njaId zt_wt$j=1msZY)^sR=(i#MFPHP>6cNzKU(^E)E9_E18@Ie)e?WJ0=^UL5nNxbbN%mE ztXO5xX38b3ILeZ&l?@dOK8PwT*}_yv?vq%cu~bE2hcyTf$SlrUn;TQf`gjb|I?5w9 zxke$ab>VB1A~>DR_4fBeKxfkFJpLq6oqk&=9u53zWN~n)Yih^X*_kJv=$>nBo$G$$ zTO(VxjC?B+RK603=JpNs?}`SGZQgKdcs?`J-7(cPzhQJ_4DxGi`8Uensxj8dwmi_n zX@})p1m7^RI_lpCX}Y|(<*j<78?#V|gks2qBzVt9Yi$5}cVkGUMIlXqINhUru% zl*yQaHN>J((jS>DW}fmYk#HSG`wRVIS$Kp0&Gy@`92vgy_8l9qD>ziUUGYbp*@L4K zSMTZR*>m+o;b^{mkHucKnWxXE75pZo%>4CSzmD@wnz#peZ6wxpA>u(!ciZ?`fJcaw76DNPBT`Bc0j zLu0)OHu}S1KX3F$BL4ro{#^Nqpx+;)f2H@XJ8$4#;LSq2mKOUu$R4WXLqOm^!mOsT zM*PXeCvF3#8(3elhXjz71keh>Q|88+cvUp$_j!Qx23}vujfQwJ<&5!%Ks2Cu~6>Ya1%*!+;R_UqTGAzgiDMU_wF+vM;R%A?Q>> z46F}fAuTQ;j_K5jl`M7KHhr+^PKn8_yaK@ICDwh|w(6?|aDqJ_vY8K8#OXca`Rojl!O?BAF$Hfkk24(Bq0XYfe7k) z3=p92X&$N2W5gNqlYKm0`D|Tw{tPrz&)SPE*g-3{;H1jQ>$2!x7iYCKRndUY?XqjE zex=MLcQZ-)fc}t9C0>U$AutUb4dOWPU7?aMULH3xvhkMitf|rxl z=W5Q6=0*A&mQxL$lz{ext;e`Uv2f**jaw}0qLmBv2|Ldq(E2(sU}IfdeOoNz^WeJ@ zJVEMnI+Zb9@n}?7cx8NPhB}oq;iF?m2MJqEt$im=33rTJXF~0m`31MiKWkg4Kn~>ZRBDeg`l@N^LD-Q`PN<#E4OYSI3 z#$C%@fFQ{Y2&So1Dd?uhv6XJJnurv`ZSs(F(BIf{8Uu$E#bQ5$wyz+RgbAu3dh;nr1G`Mn_i2HsF< z&JuZ(PuB}afg?zRC{32zNKTLu<&W{RcCRBC2sX9@l97_9a%P79IB6aEC`R0m#uKn$t4m>+;S1NuCV3V z5mU#f8mg;;eylOeGp3HW$*5nUU`U{X*po=uW;1ME=(?;k;Hz>SLX0hTjplLqWA2Z0 z9S_`+bz$6nIbRU+X8CHXt;+9nf3);i%nzw4@UGH)8`-%BlH3XknZK_V{$`3nvvVsb zThX|pU6d+@X(Di7x~Mc~KH#ko{;i z1Sk=!kPyOV0B;Dr_gm#wv4g$r=mXUp+lD=?!5+pzXQt}ooGscc8nl2g-Vn_<-y;>- zzW}V5%5i@&mT7{hYisJw^w!lz!(O+sYos5u8HyGlj6hKj5dcXc9Gw@$8I2(3)1-Ty zx|;lG%ci{3;_-Q6HrIG+VWwkWacFOM@>p@}t7on)-AD)nCLPs1DSuQ|eZH#dH@)SS znNvf}vt74!_HDlHz?(k#8HZtx2z75@OxV{FVzZN&gaR&APp=q<85;?6R-XuGBUuDfLOev=SvF&(+n?>FNPVYbgj4vp_uh5o z@bHy)?cHy1m`qd#{_Gk-wcISBdvV@O#hRyRI6U>)C((%=Gp9d+>XD z2EVaF*pcc%K{Y3Is<~RMaD^?36_T|oA%?hXT?o>e5@LY6*Mkrqr-V4hq~_J5jA9;L z_!fEw<%7r@1GSp-`?mS)h$Zt1n4tLVa6qOrM5Rh%a^Q~~4PYd96 zK=$u1*0eN}SjqJ?54Q|g3KxeoZ^ji~D3D6@Dcok$tY++XeQ~ot>h)aSz<*b}-R=p6 z{oAWcYV9FAKlu_uDC4=jivKRM&7Sga50%vLA^S^mAD+Ts6k>?h3M{DMntu1B-FEm9lG{Vx)*3&280=&;OgH$TT*F1F~= z+@wPnKrGOiL?!x&NhSFe&ygg9$he>on0+pn6(3Q|kGnxcLM7)&17V`4&CkE|r}l%G z(y1h8BSV7-wsd#4W|I@CVm<7wpx@($#%kaV-W3`vnu}D%{R@$lR?yY;+JKTiQr2hr zqpjJa1G*NQIlr~-h*01Belmqh*YSw3Gk?L~M6XS9a_)1x4aHWq*X3M43@fY|{MZU> zCX5J&jEX5x%O` z+y|o359GFWbZpCgKu%@i{QRb?C;k%=ki!_vZ|kg_OBEv9y~Qq2pw)tC3&rUW89Q0# zZ^{~_lQr>3NVq9gys9!z4I?N5Ru*adG)=0oYEpsd2VP)W(ZJE6(;KEv4&8;sSnH;a z&bc;xx6n-B0}t|Ad3Kki-UgIzSP+Q2MA-r=j~`q<81SY zaktx5=l;IS<*qCJ!%GM)_xId&ZkOwEcf##9>FWipC}4{$>&pa1A+N(mii6d01(6|2 z11ih{(lXP4HdPn*2sg6kOha8H5lt8*R_U>ncN6Xa(wkYe5Q=Ji)wcfCrt=dM=Qq70 z%J1~}w6AXYtS?kL^N!rs&d#m5cSy1uJv+PM%t-0ih*dgyZ@Bc6sz!dHWn*X0d@JOv zn7NR%w@Eq6{(Y53l@R1LNQgo9+SL&78nAi^L0$vn6IPQvGqvf&Y250W@isOn>KX#YpfQpH_;hSPj!#(b9-O4 zd-On0dNAd$v)i2BSW6Y3Dy9ZooWWNe>fbe3hy4)i75j0!z8}vBjF>Sh9fI~lLJYDu zt$~OU&Ljl(V}^ZN?1#i9VOxH3V4Y6}SA24s-N%HKT7F(+xog=3n=Nh#a!m>N6|1If zhC6PxXfQ+|0rUVq=_|WxlU7Y#0%b}AeLdKNiMoks*kL38S2sb(H8+uN%N9syz{vx_ z$3XC6Ph#dCA@L0rQXzcb6b??Do9R0^?y!1XpG|!@w!O1=zAgHzrXnc4F5;_6M7ri% zo9B94eUXT-d&8O0@v{*xU*3GWKh<9ssq=g#|6n5Byk)55^@S|~x90N)LpIH6ZJqDw z-q=EMo#8Vx(^nO-5kek1K!+Xd+6UZle~k=A2FvFbE58I7L|=p!(H0TIfEK1yh0V~s zh-2u08j_Kk1!Ss!paM+9F`Cke+ITGBb2)*m4&EUn#*|q{0YoHO(DaZoX`B>@H6^GS zc$9`4|M2!(4(Adv<%*@-+W4kRJ9k{%xZ`by`j0ic6E2tP?0U`S-ETcJlt1!1=g{ff zcSUQagZppavFonOkKDbxq0ZrQC+ykIk*nUZZ_iz4hQU)l5WpqKHanv6i1Lx^$`LaY z*#ImKmaEgjSpWurGh($cJGYajTqJd$!%lL!X@-)Y^~bhEOFyKnu+l<55li{^rD}gn zc{uE&*S;{gK>toq>fJ!Trq0c7mR`ta>4k{*MHZCax2c2dR>1>h+t0Tgqgl%*2p*6z zH6LVE<@hxFOHerCH_Oi||ASXDnCTgBOO zmgI{|h#}nzElURpK|C%Y2K36GRS-fdONeo%OPI1$7Pz1qDG8BdAC~i|faoG@NeJQN zA+~O=fP7H-$@~j^JNbALVvKv&u2G#)Z#E$&m5;3lA@KB}6?iJ|K61PIGXSx2e(H?K z5y(#(^Q-*ivw)!aNeE+pt008mAt6TAc!zQyDViYHSm3rb_#PO!1UHH~4J3D`03_e&Io2aNZ#EMZ+!);u(U1B!)w8}*S#{W97ro$od$XR2$p)MYag!L3=7PX*KYGqZLd^8qH zhlFktf6yva%22=z%i$01@j({a$s}dH820~PRtL+Me6eqVfKIIDeLyG-3sgg@^ z+x^<_>wo1?Fy#70Ew?y$C|u{^#}jH~3?vXPwrt z&+XH=x0QQr0k=1#I!e#L&3ul3+E*KNI!X)tewQ;;>n&}64uOB{otTT*yU)wLTT^Kw zXG?o0AqLo=tlB#XL3<}5a%}4=PE}EngB42%l4ywCm%p9%RDLqQ{JV>8xjPErHKyX<}jJ)qR1p1^iqX(oLA9a zpso$119B0D4UrGmt%QAnh~iMJrMe&zE((6Cj-}u8{~&iins6&`K&@6Jns6>%7lOG! zSPrG+*Y;mnyH!o!^0zo3WY->!-xAPB7skA z_5C6c0XD>1)g18kH$sE<>y_woJY=96!u%(Z{=#(<;G#O=@D`v<&|f z6aI(^4;;fjHokYUa$NnmIewJAuHrf5>&STyqOQFB{Gd5Luwwj2=J+x8`iglH{I}Mb z=aVMB3U;Yte9#;pXCEt%E5FvqH8pogJ_T4)noa1b6<8ZG z#}Sz+KSjOV93Ml}zC4>U7K)YFREVDmYnnPbA)rxl_C4cUg+=kOh-xGIU{;*TeU6yl zG)(w1_y70$IMtJ5oHQBe=3!VRR({{Z(kc;8P@Xowdjz3;3(=3I0ld}9hS@yZ&E8e? z?$|z12!Xq{Aw{g{9F_qaDpmyR93t+7!U6D;$BsxRTl6~Jn#-!Q8`Zk40rYaGv)=#* zu;k7C#g>K5qa*EYoNeB{uzPxHWPWtMueWWueYl|xb#W*~2QRkrRv(0xUb-WA5&V`; z=94z*hzU+acn0}%#0*g449QlGD!e$`s(o8-n4Y+LZu8Y+VSi~azrVG8`@rCyuKdA~ zmJLx?trK;($t#wA_L9s!dvW`Y3md0z+Tsrt>xT~Z^&cJ`J=7l$ITVMx)>+&1GMHcA zO-qB|{wD-C^z%pXTVV$v0Vxk0Wf`h}a&uAZE-W#M>_Oy{l*<;2b$V!`@c`Ph2j?$-Xe3 zqx{Ys@1r;nc2RI(c^6%B7xj7(5yg4gyQoLaV#a2AmDRe5@7#KE)9g(PQ&%O)A67P9 z*ixaCfTV4Bf3S;Z#7;JExv+8NCQ8|$>{d4ky_;hb6ko;OQk4tBPIeA)U_A-5A5=$~ zFzjhks?uCCCsnD(EtM&m_$wGVw+_`cItnSIy6y z8Eu)qY3ty=-rjwK19W78yLfWb^?QRM%~~5c`i31%rH{L8wgXz&<+fT6Xc6nw^$VM? zn;bhkyW{4KGd+9psqXGQ1A}{e{%PaR8}?2nw3yFtwWhY6c>UfTe7iN|zTb@_cwy)L zE}NtkwNdq>76{d2u>bZeXYpIwB!7dwr0SYJHecKr=T;5IF#I4mLrA7uEO3)FO{H-C z*aA)$T9rjRjFWL?OQwnH`#am4`Z9fWI6mlY^fk&Fe>!wCVH##6=XhC|z1U{oM1}r5 z9f(8%rO&_E#w%w-USA;K^M;o0cxf7toIEDS6o>hC3}WOZR$9Cd-=)2o{`YG zjV82^L=w6W*0(u7A<^Xc6l`u2UL`w6J|$nVMzV4w^cee01#}rB#aEio5H^l{#%Q}7 z9a2C?@)=~~us^VrjGCm%_Q{yAPvqxD4wJ{(j0kv(L4P=vivX}0&@}?N7K@1d>iVTj ze<`Y!#(6deioak+RM6kL@mx(xA9AQkEAsT_bK^Ke^B1M9>r-Brpij&6jaz-8@%rHk zrLR*SD0BP8S(5xwEag0;MFiPKq_3$Wp|%{kC(Ug+3^p2AAoWc(2`E^wU|6Igb@hpW zfP53d>R6Psx|(P@mcro~8;h(s%0@O4@s=Lar?O^>V9UXKe9`Vh8z>gqF`S6p;17lT z+)6(qLH>uCGs7sB54UXUD)C^1e+!}_6oRM_@~&C;!J3g1x~}MKOsC+3Ap&16Dnx~a z5CgKnLg)li`7%2`MD-K(Ohqk)s+^$2+!|nV$7)(z$Q|qLZW(ADsH+V$2buyKETS2or~7%Ej5)i#w|3u9-z5`D+AgIRQ9MaINE~b?HPRkg`TYJqS`pj>o)DO%(HV8Q5Snx>j*M zm+_41W-bd?^o$-R_bTe)2^1(J^ZolVyArA2aZEU^&X4RDNg4aB*Io$9Ymv z6X7vN(p$&y=mRX56S%KMeZ6MVLPiBGP?i0!smk80zry@}i6^wFiaC=5|3I_;;Bu7x zwfJJ;-;T18HGMY9zoUNY*s6cR3oEC#-`={pdG+{Me z6ZZw<;r1;#Wk{g$;kJGnhsGTf-*Uh3nCT>{MYX&h3YDv`M`nV*9!QYBGQP6xD;r;} zSNN>=)k2%f`aLSu*H%sChCtMDwF`CZ?sHB|i$5li&?={;{0~lUxvq9mj`?aQ@h9Qp z2J$X=J@y9P1$#(v;mmT7Zx_2y>$Kb`OgbZ&oYpiUPYsvRjo=Onb1P&$WRs|hOaQS?3 zc_<<0_W5J3GuZ%+A>!l{jwD2ae%IxEt0);Os~P*p)`an{Jk5 z+C67aB^K+Sf$by3Li3Ru+5WtwPP z^SSOW6uJ**P>308tZMXo#UTZRLp+cui-LP8%n(bD(cyVA+JJ_HEF&i1hIv!||D^|G@6G~Sg+O<8`v$eCJ6wge|o|}l4CiO)}{oFFPtvNH< zI(}?$`23F2^M2rxKPIFwR!P*0y@#Dbx;`Ul-H2?a*5TJ=3V4@z{>s=qd}N7)4XDlBLVzwfJMulBThNr%g-!fMwcKPGMMj~S49?m_?lmFpvW zPvU7!?m-`~%sre{E=qWO(`Re7B3GdW&doh=dLX0~LxR_17E6ziKsbp^_w z=*)(NmZ!49@_#BfD;d-oQ)~|MGXn8YJ^>(2egvw-2>?-^jq~vO@c{XzbXms{Qy2By za7!~yR5)tH>>`cPM$})UI+@&qy{U{@)E|~r{iwPw=OET$vV$mqm}}s3kzt3|=d%~X zr3c~*jz7E*=Y^Vlb5Xu2HslP&?c<@+7eWgT_lt6)z=7HWoX!X=^>h1jN&0`wr3xxI zEhG!!edR9QpzlV8_D<02Jo__AuYV@748FG1=@Mv;9VqTaqc0mQXQ!rUPURA_G0kby z^y+*n8$lG*t&ni?So92#?54J1syH^#--`nGmTbdZa;{Qf$@2CxE=kC)j&vgnYvP*X z9xsv_;yV$Lfpfp@#tj>8+*aZ7E7jX>oSwL5-V8RD6G5wEjUq09$IMg)0A^k?3Q1ah zIjY3&Xfc=n~XeBk;8$X#2-h<--rO0egNpU%AB;YLTJ9Q)a!AgcB)vcb-PrG_rT0R zXq)2M(6lnEu2Pg+vJ0l2Y~T!A*_G_l&%L5}kL(OFafG7aJF7O?u1FTnzu*c;Yg%ht zXZEyjxoHM*|JkeRB2SD^pk@T7RqisZL;VY3Wb6)192u`e#0EQ494LL0)>FAbQfEd* z3`@s9HMhfRRwU5)B38E!{>4G+auzKg5O_DqE=C2Ht2RKv0q^8hGJm!e2{k2(D&ull z0Bu3W$e$slR%%|e1;P&G_ruolq4U9I(hj5p)UrIXQrFAGk+5~px&r`UX!*WnQP@00 zT+-AD_!&c3G!aPPvA#&!hNAwRf}D_NK{yRIAUQc@xArD=`0!9?uVx9=1P1ozGQn^- zh<|+h*wKONxX+2B#K{d^rB8>0{Co6XsU=9Yq_FGw{X)lV!DJ}r{$U~Q?G=_v7-%}nXeIZASz4$6Ny!;3LEVwI2e!K`f3i343C2|s( z#_B3qcsZWabB>i3UZJue-AF*=FEOtU=(6x-k&Wz9?`e#|dXN4xH^KJPa?#XvOB}%#Cssco24#gdNaf zgDYUCZz7!aA-b-p8ocE^kcfsbs5zrUCV~yX11s+M9~-@t2hRu zXXQn^F_o|cnMAxnrO^n>;`FRa?a8Q#8vH2m$^n10|Ik=rPj5|QODtM{z#I;RE479FeeK)&8gto9RW#GwIh^TU=!|EhzK}QM?d`7X zZf)(ZPqsF;Ci8_&gG*2ODcml)tmH+^UB;I5HJNb+BXVBRbwG%wa^(D|AF=XjvdcS% zYop-TRRMC6DPeroE0up19<3SK=N3-E1|z5sIy8d~6xpY@MR*^b>P}U@iO4>Q{Q6q> zs_DvL1U$3~jx^&S9DE8a>z+C9&hP9Naj>o$f4C}G`fp!2>|3TEy?Y1v&xOkw%S|91 zyfU_LtbuZwz3dv9p8yMK6f8kH1sQwPeORs9VI(v~7(u2o(WtDZn3ixF0JugyLiG;_ zxk}DFp!OFNGUwURibLl;SRds))8(9}9$?iM5_&nSEzr7tMVSlj1((fhjSNQV-(Cy_ z;3dMr_39PirGxm3A~9cOd!$vl2lfs@fZMRs5&}FpBWicTkXx0b)00FM+YncrM_iNq z_hV9SC&>g79w|F2o$_%C#lZ{}2lpYJP^)IbSdQuk_X&cmP`tmwgQp5w@S*O(3!l!E z*m^D`=T%^aRtR#wk(_q)hS1%3b^OeGiMl};8k}}+5q%n;HywEmB(DdsM6_P4N=PSC zFS5pnB#s;>4^NI89-bU0sU^n^&t8s`{FdW}=O@QW+RJgn0~6zv+Z5yQz%G~H+C?}v zp$%U}K85@x`4q!fk>eC0m*a-7LgNo9cbnsr*f(QU4~f-YxoWjtI=mdO{N9Izk0i${ zzxN^GBgyfV-%CCcjl=rB*7(+Wq4DG_3sg60;<${nqV6L}+)%up;0+&X8!F1)W#C5m zNVl#$Cv83v6=ga;hEWS|ev|MD(`=O0DpzL@y`CU$1aIVC!;{BSgKNP=b#7 zV@k(~DsC#rQZ*i_a{r6w^m(gokG6bAOL)Cb?T7Aa{yk5%&F1u!a{Sv~hi0pBmzt~T zo2c^rd)R|4J1r$qJwwhAW#ysc^6J1FoHbFu{e6~@#}iR6x-VM7UKjsv zJmK}Br0ioJ)UlP0@_Q4>NU7cB#akaN9ddiH17f9u!fuch#-X^(aoTG+KA`8@C1!{Z zXdHYXvh(sOMDG$hPtfp!n}N*dfyW?v&^6I{+eExdg*CL9V*kiAL-~je45OJ_sv%Sp zu7O7zvA1cx zlur9Y-1T7FryY;?e9{(h1cKaYcmAmQ2~Tar{k`bZ{#t*y*7NU!|KjGB-XA*ck%R|3 z$kfkQHF*?sspbU#nVNvZtyEEnEQ@&)%SNf9k zps(8guQsR8S8e;DU3=Q%ODf-J@OqO=1F>bB!^i&tZ{T;S*H;Z3i*FUUzbJ8!Zi9q- zIZn8zaZMfAXO0R9Bu8`U@@S_Hi`sC?FPYDv9g)!a@-u`Skl#2)rvVLUk#UpIqqg!W zNe4MPGDLeiiamV}d#ZvTaW=Do==UW%phAAf>=V$Nb7 z0(-aV*!!wjiySA3PvfvtFQc`{S^rKSwIK$EcQFoXSAHz|EG^Sh*fM{GF|K|{6< zhT84v&}am2L4}EI6j1({3A?{o-`awd8|&$6>2K{%H`FxOX2U^YP2#YF z3+RP3A+4)h#c4En5RrA*z2sPxYcXKwRA}dW{n4&ddz&|Q^aN@XsjB9&bo6>ro}rPY z^G+E;MweuTPn{YKCac=eEU96>xU`Ie?PjG$sSi5a%&_aYIj?#JZpIDU4hM=993F=p z$O9i`JJn`mi%zSFloZ3^5Y-uIN@X~#BQ>3RlRs|W=9oaA^@vzZ3Tz z$b`b`smu7BIrZ|4F)w|>6*C>!*JsX@B!U;5j2j5hO&D4L-+mlF-UA;lPLW*EPnf!| z2y&!G<}sPlB>T9E$^{xz)EYzd6UJE=o6TfJqxvaXZq`uI;9K%%;_ySa$~f0T{*3pW(^(sSH@*zkHy&GYc2WQS zz3PL|3={!DSGv~|{mDsC)wP&JI4Js9#CXhFISt{hi~e5d@@~xKBAuT_o%ieX+1x^} zK8AJ`m=Sg-zPwv)!+M>l^z z`CIxKj4Zt=Qq>@DkEmDP`}_wZ!H>`z;>RtRvc6O3N{yY$3Qa;gcN?8Y<$o%0nP0!( zGxeR)A#(ajXD-j;Vuk1$p#BV!)d%Y;&&G=*y{&GvBSlD2`(y8ZH+2B~(?5mSbETZ6@9>DT4%H}* zXSmy;INdcKzLSr8s@+te@|M!g9uB&jn30`eOW&BGCa_MIhrgCz>*g+ZwWst@>64xs z>}x-|LKGn-i|jS>Dj|kdhyeQWAB>v{wBbIcS)4%!AmEFr$;z5Y8yGy(E;0LY-5^ew44Y(f4ER zl{Q1Mhy&NzR6{0Z9nq*eWG(%&q4bnBwDu+&F)tXZ=*0;MbjXxI==F@(lj61B_Xn>} zn7ZU)%reH%7ojZ1%To-8aI!~ZtL1nFv6#J|m9S1JLng*#zh~wBOR`HbQ3yIS;oEO9 zW+Qr7qjR!&O+Kd7bkIFaS^n?k-zo1^-;`{pLfL= zP+PMS1Z{>~s9$nYYD)#>??q1yaRNkyunezHD{u_P_*XjHd={58;d4bibz?Ce_NS8m z(l3)oE-!s11Q(W1*z24Lk5_ftZ2i5;T*~K5Ej=*1@DU}S42F_R-$pH;p>X02+jrl(Wy`I*ciysj^DWy0=&DI#06%wi?znn#^4yNj&K>6_C$HYo zIWv0Y^z@n0(KFN2SB}0X8rYEJf2a|~-%~XuRy`~Fmv&-9arPX0{{ZbRTfh&xONL^X zloc=*NwG@>EBE9?{5^{tmzLz@-f|^NvHaqelnZ z)Y)slNOTJOhdUPDcKn_|IO4x&;oL|f*U-PStMprg+eMR5SX;0@+A+@tI*X2t;OtiP z9Z+I0Wp%3u3YW7Q^rj_t)-0@nH;_#T?E#~rLIRmh(@E4qL#S356`zzm!yl$8j>Dk% zL?n2NIfY}2X!5;uOxPLtZg7zLcVafJ#Vk%n5w2)D^6nWSRI01Ywg(O9+>V*(j-rt8 z<|NENAoGpZ?dTtk#>}In)uRD(5=WFjqt{EuZ=+ZGZcCTM3&V*mC)b#Et3D&Ug%a?G zZFKVw=9EfOqBhf%>`Zm8oSp0C%ubLiU5_KMw(eZ72u7~B9mspJITK=MqJa+usZA?( zRUx^zW>+Gsfl9YFX%B;jAb!Ou*8lSA()M((z^*0rGgxN^EKyr~X z;6}$fG!?`3Lew2jaDuF+6q`#O>x5w)Mw5+I$isAGo_n>=5r{r1y^SZIl-|aZuxkH? z-2KuoMov%7Tsbn^wz;=wBaBwj4@j-WTvq!+`kDXreIbt%bg%nDi$wB-Aw6Y86`gD5 z#WSJ!*Qp=Se+HvZ&R#V(cGYa<4pMfcgOku_XR}^KC$Xc{p zsW}zUm240_*gBFOm1rXgS;-OWPOpM(!^W~|1h zKHbtelN^aOW)l5!OnUJg&=9tOz*c9r(bU+Q?xEfWe(v7Q01NtJEzqd@zf2 zC@8Tt){0MtlbNK`7H?au*ySuv-AEKe-mJADlso)KyXRY5=evJe%eln%(t#H;EOc6! zmXxuMnIY)aavbZ(upY4v)J16)JN#vahSi#GriloSRTlvwj+ITP>d^>V@5zEfL+7fk zFnP(`6TxX@VKj+zH+_idn9}OYH)bo4;Yxo#SKn@K3B+Gni2 zHPwKF#sy~qAGIazwpiQFynY%=H5D?-`KbXN6+j?5wa&#^XZUU2NW|Oua<9+gr)DDP zZ&teFM&=cTJK#=rcflX)>nd~?GL2PHI7he) z%#MA7dczvPARlGRGF1wLWQ0#lU+AL(4|n^~fug($tT=Ws>x}qt zMVRU+T)%Pq9cP9|uDtysZ5qGTneDQ9?EbLDnQF`(xNgJbbqD&ADO?d|w**4A&eri) z+__`tov$3@=fgg4WJb4u={6AT=QP=rFXKiE{Wb=CViz$V!`Q%^(JX;$n#u`3u=3Y+FE*(qJ^*}mo+YTj2=Ka-jxv50sT#U#ts&(If@7Q*Vt zt@3m*(!OuHe}BGJnY?mx&ub>5CD9!f#>;<^^i&e`v(kQ%7L7svWW$s@uVMbU;l#2J zdmlnHVUXP@W7r75GPJBizzh9&7AbF2yFhDXiw=cWC!Fe56G&Adr3e!GsTP4kWudJMuODaj?uBRg-WYtU}rx$BP8 zLqn(USUN7X?4yrHmBQ5d-CbR~&reNWy|b%p=hbVvdg|?B=_VTC!xdNeN(XLc!2T3h zi?BM@!d@qE^ZA-;)E)87HS`B=DxxzlbgVLgIs_PBxV+#roxY}g2uyQ}u6U?i0l-{2 z4=~7fD+I?{D3mKS)YB2mbh5ssp(R=utMhx%so6qDkw`R3O`0b&m%{uIisYe_h!4fk zr7Dzzngubz^i?_o(Qi(?ViNr-e+sOW@--pfgIm6)`Ty?AU+&x5(Scrd=xIE3Vgk}q z`t6NXydhHhHhPA>n$I@R=Xy3Gkq6peen$Bra8QQ`R-WxDZl|-@L99gCET|qg#|b5+ zC_+@An3Qbv74esr=7xHBCLPWBmcGVxeYPP>*O=gdRUNMrTdxVzK-B37Zm=0${}DV9 zbU=Lt4Vi&8!$9TV4n;!2fqglY6l69PYub(v&tE&)Hh;AAD{46SUBL@h`swH4P%xql z9UVpGUMLxzug}ihv~BG4)-KQ5R)jjYE8oG2J%}!Lki<7iOK`THVKX#J(SYC~?hjL) zNtL`d563-_3&5=gDPK|+n2=quQ{)!~rK335Y&FOukj+(1wWwZT8`FW~Cnf(R9gQ@tVQXRr;YNJ~s5Yzsikoba0-{dMIYd zKgnxI(wngi{v_nK%;^8U8m7!jF=@(dG7HVnxHy)BE6^x3j^$>JPIl-8hlnUB4PS)i zqbYzlU@iT^8I0i?VQbwt<5L|~wb5WrCis0V==R8-c9$!S(_3s-zaQ6D+J?KAzUp&9 z-~5-9BJlhvey*u=pQYZG=(s12!%~Fn8Qi)<(fe-FElud>(1;GB@b}F~K4?vxG>;?q zgZ-lHgW$G=3P-agOZU8lLq?RJcFr(ob#sI4B_v1505`d;lF3$P0R92jQv?h9bNPM! z9gFR)C%{<~ZF5IgCI-rEHMPGzQM)N#FS)7{c&}UDZ3!Y~H_8@@bNL!YL3uXLxWZ9( zpjd({E}L@FX;(B2T!irOqHq$0do|FHb5rdt*~WCTAqobkPJ^JwQ9kOys)PfbCQ>6p z6P92aA!{N{BZLhRsJme~Y@r}gwCk4M^)CX^xWjLK?kqXAXG_0!q@9&v?BARsH}}l* zddT`0UApg0)#V3?%!oKke-6rA5y$?d5x(X-&7k#z(EzdlupRs1RZ`sA&Ni`kejc5^ zQBb0HHtJ!}4i@`)gr-&7IdoJ7dLUrv7Fmkxi2a69NeNIV6mSB~whMqQZbQHqs3&e**N@%8JF&6eq0=!-Mc?WeQxXlSqtMbQ6V)89{2)bywaW zBuET>8uSQ1?iB`!p{I#XVg^0w=?`wulLB8o=m=X&OV^U1rhiv+zY#|MRjWZhR=Yty zM$lMNiijpZDB?Iq{8;ruN_+rP!ls@@e~o=Ie!Nefd+;IeGmKT*A+TH7WN|#=QEVjE zHFUq!Y&h_M4@zDlD-;12XeKg25ol?I0oq#K>PL@Z=w0kQSrdkCil9rL9wiUsh~s)u z@*HVW(C+Ujt)mCaT_RSlEd7BJhk+a{)>?R))u|V1Fg?0~tHdAXQ8xn~32j#VdbOls}>?7ky_>UDr(DWXSpwZ{S z)OU)^=R0WwOQ`!bWBbI|biL)$37z*L1B5@g+w=!1`-XfQF}8&&VFc0Xrzya^qE)_R zUSh&ZkC5BVkf3C~mw%Ho(vMprp_l6qn#}bw{Xx(oEPT;>QlsvV6|Xb=pQQ%dupO{F4fv!U)5Q*BTu}9Yqr6IFMO?DFl_FgqS3OoA|FrU3)$>(`vqBL6H=z}j9c*Z6{{%wLp;h%fS5HzqN>lFwK=m!YAUde8#%qEx+lt-2jYwx|s zseHP-9bTQR9;2POO=Rn2Zu(004)M%#Z{RUxUrO*5yV#p?Cm2!_P-T>TsRP#OPz%*Z zSS#|cdcXh|-B3Q}#h_q<*1RYHJWk|nP%)Tosz;8G90=CMyL29c-VzCHQT0!nN;=h9 zbWbPyn+}=iz0%*}&Vzx-iwL+`RO@Xl?Joi}MS&9PKY0 zf1|y+tKN;Apxqm8jO=>*;Un+ZwfpWP>FL_&&f8B94PW{8eYjL&2$+`nc*3-#$Q<`b ziZr`t%j`8lykG3r+HS9e`={szB0W<&%Jf?@&5kG%f}SBWr8!hvid?GsEzz&s zvf{=n?2&FCQ8O4&=U43KOdF9ikzkG?SH~8_HnbKV^Nkb%+AGR<3~1B^EtHN~|2aX%z%z3J?_l1nhazTDVm~msE&E(rM8`E_modXrY#}x?PjW=giu`wTBtA@VN`Z_S0-O1XyBxD_R$| zAdxr!s>uyEY!N;01_t-_^z3_-zvUfo_g86FN9cX~uI%R4jW_R@Jv%me-R6bsr&e~l z+wuBG28U3C?XR*GQ>R|LaU0#T8TVV4#6!kpnM68O#dJ&y;Q3pwP z;!sG*4)qzAbu)onlq_}x?02_aVZh17$Q7OB3k%9q6iXKY@yBh!Je>!Jd_D<<1 z5`tg$W~SOYXPRd_+d8^pZJmt`9WzbE)`=pvob})&*b_Jj)+_3upjZ|m5HWdGl?Ug9 zqurcA^AHc6^`{g+TpAI}L2y*ouEF0F!i;iBy8nrI4yFtU*yVVe^Wj5~jxJNrDR2UwEnlNko#2e8%CE(lhhK?# z<}14Nj){6{8b_u2Hrf|d4lB>ja2)wKqdFUojScMWO%FG=ZfGtX zcEnoZo;ruk=BiHhb~g7bA+I;cyVH%G4RN>CZmaV&^=7A9l7+PDY8=mY;c6>TlRT@E z1~n}T>X9dMe2~{WFHnILf6)pgoJ|p-h$xXo z*jf$&LOKzBD5^8*eCDSJJGd3+mmYSO&IQ6@B(iS0DdGgpXk5c_1XqpwyFYN%h!!qH z5f9H031Dqvc7kK6KNp>i`W$|9)_@b1Qp)P*W^SeGm^f9dx?L1crm~j_y7z$6PY9{d zH3uYlLj64B4r|}HxxD^r`#*L(VoUhEF6~EdCGGZ>n!Ry4%)q~0>ccTGTinaPhjWEu z7N3VqPf@jOeOMG8=?1{ET{0y7(3t2_$R=c~nP1CHTBAF7(|f$ zQRM;nZvoUn^{@@a3FdI1#t-|XEYevM83a`qaX%vx;<$LxZm+SU{E0HIjXh00X|&(0 zj)#JH0EBG7jnw#b-Z&#~2p@wNBz(uIM2Z3K}i7N%o$g1sHlq7zkA9M3pf{02GVxY&wQ>8G*>c zmhr7w-n;8IVh+szFGgN5cbNV@y!Bv5x_!F&FDl6t^VktkU5rQU!3FOrY>oTT7;~J`wM(?k-nz zQFuz2M&@GlkYvoOkOa+>nlawf)B<8 zaW>1|A#s)^JP5+^aYYbLjXd%xE9`V6tVy;FF%Wq z8W_w<3{DnDhx_}wXM1MX!=i6}qIwZ5z7VOsEPN`nfpB%U(ICE;i`740ha90{v&+Z% zAAs9c^=U#_@FPg<{qO73u;*y5Co260V?O%pebM(t{nsHh`Y63B?ckphzuN-Q>!`;8 zb(Hyy(uCQBF&}x0sZpN>TL@*4vh4tZHq^YRwmQ-fZSYd#H=NfsLN@t;;iKvHpnmcQ zCQ&rXC#O#h^Qhk+{m{g*!r&DfCiZNe`cQV-*>~9}9}b|b?~cx0g~HyR*^!ZfvD2B+ z6lsp|^6!C)(5#q{Yi!OmpzQ4h*7zb@K`dYhT4w+9@@NV@EzOv z?Ib!NF`Rt|M`(W{!8C_QIL){@BxV+(=?1{2T0O9rOCqQPYxjd*9o&vyk7>Iom%lK-7a>QSZhqsjdxrkP1B-k&du) z4aIE;oFSWelFEMLsCTj1*dhWMx^LCIJ{ST5R2xIE2uD*P4R)wGOC7$lJtfTedIWlA#|ObwL-S;j>fLQ%xlk>XB09Ly4I3VK(^M-4 z{{+=6%&ddMz8Q6JdSOIsbE*M>^qemz%Oc2nxnAcSFcy}DZraiqqm!D#8wbX_k_IAr5v;(tWb$zMNRsOR#(4aMdUS8C<2t2&0w$E zGP!Iou2>J^1OcuQk-qTi+$q*zTNTWq9IKERa@|}zyV0$BoHg#MiR|$H-mYB(`-WUK zF0a*@Jvp`+!L--Bk`9}SMO&wwwJw~Zibl`?5qG>y!r#RwySrsCt>=vuOD{t7Z`L@< z5M>1d?-o%J>Ryeg$a`tj=!~rKI7XpKfefnQlvQpQ=I^Towc~s;TsSIqv!UEawP~lY zy&`L(QoI;4$0&EJDQ4Hg>h*ErGbSXe!TzY)xU6UrC35(%b}FQiDzT~O_mt5-{u^~dPt^018;w+>NzGjxu{1!9#gRG$r@o`wGX`i1-cfqEvE zm-$nWloZv?vTLQROjEZZ&4QvgjcVgGlpfh^qBbtRP<9%c0i^&6xTp+m? z6b=JblA$^dHDq;(n(DRHakwsejzSJz1NnOVZ~T!z%WZ4#**EwZ ziu{&tM0D*f$3EvyfARL)a~n29g70*q{Jn2`CoEtzk)?RtXOt&=eEEstp1e3)7lw~| z6L@7*Rs_Rd-HbXoxdc1LR|IcyPVuoAxzw5~C^|P4`ZKaZ+`cb0wF(UmjqWI4@ckX-F!2@@{qA0DY z-zdFUqqIoSov8zqkK?`4;=qznWdMUiG-s+btAu5eX4IGox3!#F@qrhxO`2evw7(09HJ1N`{=eq(6C{h| z8U;TaaVUg6%_3Kb>b3Lcebf}IV?#DG)7ic~mAI@A>T2}l|5UEwAu3tPUp~^mt1FS* zUR77TW&srj53O1d#e#RMwTNK9WIY~C9n|9;{iEygM4p2rRzCeQaw8lJCec4)(HbdAG=s*ip@Wd(kz9z9OOw zk1iObTnP@6s8X!oLk^D|cZ*|{cp~Cr=BS~AklE=o&O(iZvx*|b>^i* z$4P)B&Q1sdGDD|tr&B_Q z!pz%F7c77O=bWp>i9^zU@AvT|vUGKI@44qZ=h>hC6Wjy*DTmqa@z~E3Gdk|F|IVSR zj^Ei`;AY^y^39ly3&<%XRsVu>3VV@Lc;2TYM*KK?+ZoSvEkU_@^_yXalF`i z0Z}ke<7YYU97l9gu9OK`n#ZM-8F2FDGYKKUFEo=7_BOP!D!IbB9U0}vi5pHc=PQdWtVU=P*$ zG<%1mgSf~U8H8o^{|$_2wt=`s;JgNOi8*}qJoYoR3fE$*Y?A@sD__OuycxQ20(0I% zakiibg(37@h)$DW-;X>o$aHeQ@o-csNp0ED_lW96TUd(`Ru&F^(ti~pHcNrQ0P<=| znlRi^NlkwTxhC)UuB@XU$X`EhuhBxL@7zJM^BwwWcrwukL}%YHJG^?Jcs;udo{~ptW<+Sjarp7CmhQuT;hBqy$XukzE9C*f!r{M| z1i_)d3_dsV&phjZqFd}Ym>0^?djx+Kx+9PM2-MRz&QIjNhf02;0cM&3m}yT8sV}01 z;|1p@QsT?mL87a@{-AF@7d(89U=c_GQevEP6EAwhxKT;No(thfk%)h65g8~n1A-QMky}X`xyaz4<7i7oo|z~etzc`4`~=ah zSsbNv#ZiGqTAo|TCVmCXTBUQj!qFs_%k&c}tBl#=+I28MnSKH{!sRsi&D48OWcu9z z-(!`BPG7Ef$8PqEU!SXoz<-a9{`Q=%gHjZja0rbkFvlS@BH4Ia<0wZyG$O`%BJU3_Y*mA+znLM_;GZ_!-_94g6Ie9Fo1WMX6L<1r^ zGg}KG$hCg$!c(?R1%2-gf7%!P)E|+$1)34@1DXM36@uFkZj`Y3IQBfa5^}L|o(EhD zD5v=RW}GK7yS@Q#?UM|&YJ%;eK3xXLYN@KI3az?Y42o?_b!bs>Z0!LMr+JAJs05ND2JzouLkPMkW-{w zL9TfYH`heL2NS9?O{SD-w!Y4d)kwzbTI!qKweA`{VAN$|z@^$3p-Z6WGu9?&P?iza zGd^vh>JWJiuMtt*0GDD9iT9P`^4|)86fIPI7@-!AF5`2k_y~nm=^J{wa&2N2&eHer zG)r)nswcP#XXy;$FS!Ug&JrI*oX7-9XB)@MqfKGpEIquiyNuyi##zE%9K>AShr7oJ z!v+B?_%F%OrM-_igcRi{u6&^h*-*7hF%~t8aR5&ORg$6v6h=wRFY;8CCNYHu#z|(wOvxf;K$V+k?dD(UI;hfD4mpuW>KEU6$>K%ep6k zlO)QzP;8QNfKoBqY_q6MAh%{*vdi`pZ+vE*P`Y9C4G2eG7jd z0$Kp(zXS7ML2441D=AQqAD;fKG+4;NXJ=Pz=)Eb)S6WdE1tOy2@RVh?r8x*2v#SHO z-`P|W`b9dJ#_xRTn9~M$Has?b-^3S%PY4cfa^IH^prE7IN)KNZg8rVPVPgA0n6N&^ z?mn{O$US4^1`7}Dn5cKSY2^6#ovGx=`c~kt>fT?^-M((!?Q`ON>-v%8)OSiHUL4Q~ zyMd6m(x_`0A#ZQk#_RjRNMuUi&Tt0?_Jaiq7T*wE9r4E6h}Q$(-Uu7E8k>`Bm8`LJ zrKKd$PqN#&tqA_Y-TWd3fj}pBf+Y^n7@wUvI5_{YHy!!d{NTZvZ_*z5cUUu?RogpH z-u2Fto!eJ=(9aO1?7>v{(%WaRe&C{u9=Lk;_DjR5phj8Ld+k26Z|+dx_+R|R@xq}w ze6Ryvjkr!p{-)$9ut)|O;qN=Aku?3^8oD;Mb;#=fvveGbXR zlnfo>+~;tal9Jb-^PJBin!cSn63Z8h(cFIKXkfMTot~0mUD2J2#j{3pN~{*HLIwh! z>9#Y+3o8SiU{Q%-jHP$csHQ1bVhD~xXNdcpQIP|hxNI>m?+1-{lUF(gU`7>^23(T- z)A0O~84FG+%PARZ3Kd}cy&#Bg)@=bnkZX_DTgl~^wZyIkkpozfh#fV>fYAhTP_YFI z`NKgi*4~t^nJ)feD@;~U0C2Ys+T3=Z4_KM6FrU-r2<~qhDtyE51H3tNg_~+7*+ugJ z4mWsyNGm>yUs#aBQhcTOfJd{sbpM`s3zQ&X4?G2L10fpZrSU?cANdxY=~P3#>I7J| zs5Re1am)_Gh~ikop!Z|~-CBAQ?aB5C+J_>A2PwNY@rd+{^eiE(0W{6y^@wzY+)9%r zozD670G<$U_-XX8tg!m%MB(V%!CNCek+HeX+{TXXi~74Kdz-Z8KPe06n~>wWv~8H?p4)= zoo#^tfHthlN3nX1(zvv?Fy%o#eH{wa@SI5cMY$%$=GY!C?I+ ztF0wE9vg=#V3p8F%8rNLEI3igL_{P94`m^sZP5vfHaO;j_B-)|va=ucG*(Owl;xpo zW=8jow09Mz>Q^0^=w96&wFiIRHIWH4WY@G!P|$9qd$hiDs;O;7v!9LbMz<#Ux%Ed@ zw9SvVuMes#Hzn6xGaQN~)Z&j}k7pWt2kSbAqbd-8g3(-}ZzxjRGnM)E+MTqKN}d^b z`Qj2Vr73%blA#F$;tHzTNOAP#R_dk_gg}$zJC=M)xha0ik3xIOxE>en7Kpr;ZMQ(N zmoneBFrYPjUo>C%cFVJ(`NDVA2t_a8FEg4jATCthgyDxl-%p7f{jEBpSw+rOX4$D&PWYDBsu9 zkx3(|HXK5w(Ks6?+67PIOo_j83g(Xm>P_Ks4PApn%5aHg*s+NB(YdsD!KZpJxuU(f zr=_|3^7TVoR|IUheJYh+ z8T9o7z@$Bt^w~TXSI}ZX!}CmCCI#GzKu@fz%jrk5xUSk=Hct&qSSIx@2HERa{~igr zui&&iZvYE#^sP%l7D;BTyV=}op2hnfsi(a!oz$(iS`k*Ku~RHXJaH~Z$s^@eCB930 zveaJdq7drHn>#bv+wTVS#a+dJBbAz6@Ao^@M+YxzXFb~r`PGT$$(Lk2q)(RH;;}Zy z-g02_mO~xK0Z$IB3a940KfC{`f&AJeph*hNtrHpAg~W@fZ!G22ptA_Ij7?zGL?d0S z9XTQ>hpdBBatiTI2~ibLa*UsaRpuv|DR)Frd-x$)u(qt@bRR`fF!NO=w zA(-B%w=TS=>_5vrd8oNAfPYU{r-lg0vLNqM{7AO1k$_u@j4%;GBNLfZX9kajz)5L? z`h5%?pi#lA`)d3(4x8j<-pbzN#8%#Hxmbg&#Ds8R8&>RFlb(6&H2zPozI;W`)*+>M zjcIV}``KUMwTC|U?Yno^U7y*w@iXt({hoc;Oo%2 zuY<8>ntW*w`8xQP0UUGPU+_I2ul} zkg<&RAg5q~^~C}fh1u>SBSCB_=*l<);Kkmzvh+M zl*JG8*o|zZ49k#SY=}!iYlgG6Jf*~VC`W?$4=53`kvte3>$i@zb6A-V-276FM@ozcJ-*XS0+C$^l? zkSLeDEHAJsf~txAcuBXQk;R}K0(Y9EJY&D$(J>49=^p;LuiUtCe`wq%_&o zH{92r)B)hVEVnY4mZ3@rNklxD+>3%CL(QNx4B}wql4UpR(9l{syGL%hr*61=B-gyQ z?-QJf-R##-azZ+oDv3=11kWEmw${s)=XR*8FEPJqFfme6-j9x zZ{y3+l#JF#bibDag*LNh;sLyJcgYhckit>*ytE}LspBVF=*<*8lzF3)8`B*gPcY(h zWT%oZQ!9hVNgWCT|F8J(VpGyCt)=}TcBf9=P%&e$crapGN$Mxt#^sEm41(pJOU|@O zZA~pr=ae&8oT~U+5Q`(UM0ROO!rXb!MplcdV$ag zmPs3=x~Q_$OjI}NhEu<)N%t5n2wOoaEbrHS^W@Z9)|dKqZ|m#bPW`&s{bFf6l=j1O zf2dPbqm(H$6MtDkDkZL56AJj;P8e5>tU+j>oD7x>tC~#MJz&uf^U-{WM)UY^XI+U^ z*Pclk7zJ_b2|kKsAy#I9qGFqqs$uxR1|YV~^{6rzJBzDX*xb~i0Gb*$X62t=TYqqX+h@A`@95@ORr3aa6lML3nm5u7AZTZ-4b zwZ2i!8@x+@Le1L~Yxec^?OU_!&ef~$`1I~Orl#)LEsm6H-d5&zkBsadSXo{3wsL@u z<%<7|nzw_*kzaogm@$)5kTMG9HAylr9`+!>3qb>h0P5r&2%zLJDcS%Y;FN&fLz7Dq zqc7(_JLoH%j0xlvD3*0z-Sp)zn*d1d{P2h27zvr5_#1L@1=Wv#)NxwZoHn+O&DtDV z$X9%UC4C{yVJm*2_-T{fqJcZK7Jo?4CtM0Xg*o@}%x}bpkkny4fwEPM4@SNu?@^HP zA*Yb#XtA_fkohekOCOovy%dEHz=^TO=-PsW z0Zq@$?CGD#?VUyXrl#jE?u__FIziB~hBeO)m#AcL+h~+UN4E`?h-h$q3oCvv=v5BU z`LDm5*MmBML zh!3W^hiW@JaoCNo?TMyHcIRl%c#BhQ8MoxGnH%1{y4~TiTTFpkweQ$u*GyY%BKHZ> zT0W7Rh_%ghO&;r0YXc^W-Q#Fqy?c1>ntZgUwzelq+??xl%0_r{t_3%@DBC-5bnykS zmnq74H;(q+K}Y`@N4F~bjiXj&=OQ8thQ2nzuR+zBR%N@iSUJwGLdOwR+*~=Et|E@^ z?53k!4)SZnvU}|;m2<^jw9}7od1+^5w3CoqoRBM9!es|F_J}o0k;JCFAo}{ z`js&%*c1HtA>;T0{t_RnL(kJzcz7UrQw<(B!~qCEQMf_}6@(K32%GOabi(n0r?#_p zgQ;Pqg}sDSEGw3lO5D-Rb)Jsos{3 ztv^GtYLF>?abGIe_UOa|bPD?sz5dIhQ*pQ=&d+~+aAR}FnpRScd+=gc&=eYy0F+y@ zYqQ)vAsBJLgdR3bsuo^Gzd({sC5OyeIq$4~?UlLQm1}1XM4vt#+v|%POm4=D+D=l=H6FRul{Xw%6#iy7#`L2Xn*VKSHD=Q%lRR@R@ zFlh|4KzGfEiaLN)kj+nqA|^!iMBYVw2~z>)nnb!Zb*9O%R+

o%#@ht8!2nLn+UV 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 literal 0 HcmV?d00001 diff --git a/app/data/fonts/Comfortaa-SemiBold.ttf b/app/data/fonts/Comfortaa-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..faa8f5c21708d5bd73acbb600ece13f22ececc09 GIT binary patch literal 140144 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 HGwnmty3 z%=u6JdNS!_fgc2%a8{iA`6Ln2_pdXX8@)c~Bb6V={E(Rf>*^WIwwYb?NQxUlA9EMt z@HEp5dYv0V*NTZNrgKukFq3I;-e8ub6Dv2b0b(ZjD@=o}f-RO2|7%3ZAn12FY*s9= znKx_kKjD@j0GOdTU`-ztxgfV9sU_ORxRXz&_^>NRFZ^*|1P4SN{F9!5ZAy5i%UEtw4_L@kBs zmZ*D=6y)GMu%fHR#d0Xy0zuc&(x2^bj0gQrdv(`HJLa+!C_n&#;vFLNkwzf8AV@P9 zfRJd>JWfqXKHNGxXg4`Lo`l6(OzbFk?42m>?oMA`-g|EPrph&h&p<40jr3=|aihuY z3kB}JcWKk{V(VjHGX|c*wzwbqwHVmF$T=QPE#*I6vYQ# zoR=nTh+KMU%7VUY!a7zCfKb4r>d95(Fk%CK&YBYbTp))qijVtF_JYZf{?7p|Gwa1Dh|uCb0>N$*`bg5Uf05IOdop=*sMMC7+ch-Y2yhc8QOQsHNqPn#)}5dHWOaV^pQK%&`ALZC{MJASev}ZS z>}O&HYar@&vdD|;?Nc2+m)RfJ#RB=|5>ME{wGfr3YkPqGn&n->FQ@3c@XO(47a$p~ z&?LhMyJzk~1mZGEq2MK{=qwP4ke3R{ON9JlQhKDNS%Og9k}&tk(;b{ASY?SKa-#}D zu}M7N2)Fs90A2@Vaj_h0YbCLg?`s`t8>tsA4br%oPR?~h|YVf+^6ZDMmBCZpXTwV~YqG6CBFEOdkh z%PbcCbIfLAOAeErbm%;Y1v-tkmq_FBal4M{D2nvDGXR{gc5k>#F9YiEla-K90 zCVEo;{PTZm5yX^9r#KrO9zv+4x4S);noO6QU~hTd4m&hfk|&+3G*&Pls1NxU16fm1 zQ`Z*@f6re^zjbo5o^raXiYn|@BJsMt+>{6fJT8QUkxs7K%Vey9Yea55 zlYt@P6Yjf6&WNDF6I-q?m#^RQrfBp{JzLt^w)DJ7&SdJkty``we=iW@r!d$*C+Lkl z0-8%0QSJV64=B)RK&*vgbcl-`ukknKi!!NLB;XTHN|=Z1W7IH$C>!?dlL|!N^eoeg z1r83MoZfJ}^a{+(l$~tr*nnW8s#`V=-em?o7{Lhmvm$TXRq?6-UcRF$;Ln;n5dFjz1Om8N0HpjsYPGV#MjW< zhqhclIeGn-JEQ#7PQT^R%m2<3u3UR(@8k&E~3p>nR~?@07BZ{d$d76>AfogM=8;vhp>(OCC2?4_ONc@*LQQbr1HG(8uzV z6YRb6*}CN?@kaKggebG~_1_MGHHaw0X*I}7Y_^)BR8?h3`nTj!)TJoF$?$YnJBJDggWyXm&h`NP)SY_~6EGb$DW}VLmpREVfykH!hWS4<(@c#CpYk+^y}$Zv;m4 zD3u05xiASa#O}WcB1kxs5ZI3y~MjrIgBip&kaU?9x#Z%2u)&I zLL38{n35Ids`f=JLjyD+8EGr70Bx1T{31&d3BUO1X9I1 zjHCsvlD7;rO52TJ*m=j{VkV|svHYq*zWJ8@U}H*a0I_w-2N zit`OaC+=E^CU<*wy?pD=yY^pp@6M)}RdF~{*4EC^D_^&N_iIm&VDDX!ZLffAv!KEO zaXxZiIihAFAArTddbKui=7AAl4Hyl~!Y!mNmq_L3v6EbGT2;|+_QemzD&O{p!+s?} zKM_y)_HxD_R~`Yv@!A&wrE1>^N_{<$Z&J7P%}P&XtMo*~`$8ts`(|~By;AT%&Hi(( z1Zmdt34#ayUhsemvZ{VuMBA_xM^=8Rd>5;1V$J20TpdVSmm{3fj$l8K?WhV78C;^M z(#27H{@~^J?V@n&@bRhg;UUCxB0KJJ&z{}MpCWUn(puWn*SDuseYNxKELJV%Ab9@) zv8qA7VGU=?SrP|Jh++1<=Yc4(|5^`1NM-qq3C*jn#o*<)P=%C)$g>a1XVm02^25NE zgb;pSzm)id#kyw~*pJrzcJlM&v&XsTVhHu1dXEk(vdq{{;Ex*1BVigvOT1;duhHLv+W?#G* z3&1e>p%PDH>@^obwAJ=N_@T!T+ZOo@pX3rrPta!CGe&q+KBI0QiR#IZlHXEYi>7VB z7fx_LRjl|aw#gLExWdk_;(pFt4oE6dc1f(Ugg|~4;z&eYNxGX+cFC`5bX30K2qcnr zZgz63+3$%5Y?ZH@1C7eNldXy6S1LDn9IcV%T`m{C)eNKRt@zd)yK3%22!2=wo2aN} zpP$@@&xAXXeTisFP=Nf`xcf|B?@Igb=?TQ-S9r$2F zOVn-ttU2QLMvU(YzuOcEc$B|sZ1MP(?^g~xQDU{Kac_TVjMLMnaUhe_aUTS_zS7KUlNd@>_`dB}Ac050bMW>-(_Ve+9gI z5bbR#r=jM6WK_g{dzHYZ|M3-fS~<`oSTeMKMP3^)RogsnVp z3xsSd?LTw`eIdp6o7UetdByHE|2*?^BzF8E+xOGY*pyJT*~RxY^FdcQes&HrNIb5}Ji@BoIN(`>!h_p%CZrVC=z& z!CtZRFOb6@Yj!H9LtbAb+U!`qKjQI+2oK`>#V&kI;~sbY40Q;ynI*&!`@nh-BJV6A z2H02XA%N@gm1mS=N)8pFNz{3sEL(Hg7KhykqYvqjZjlbrV*lgw)|Zbo^FqZJxOm8riPol>&gWLu?Ea2%Fk8nLCIEz z?O}w4RVYH1nfTO}8#Zn}yKVa|o0~UWH$1q|*S9b@K$ZrN_+!4gsA1E&?K{qH-h6Jm zW$R047xMdyrTzK*{!(#&en-sz)}_SvWj0f+RUHAoJ}LC)AS#>)({kLUj}L?5YU4lD z#!c$MTjgjAGMfbbuTl%c7O6r1uMT~M1`Yfg{QBrpZB%_oA3bz)?FkeIm-8E9udRD> zZGNRy<5WE_;Sa;lTs_0juQS81>F`H%c$y>A$B(kx>%R99ZQP_Ddr57UV>l- z)$bHG#A70sjhusCVlkNhod=_?ykze?n@L41=f zIjwe+&8V>$RkvLP2vfs4SOo{Ls_|Rf{MND2&JNDD?wQ{+yz#hp5~{Np-?zJ6#OhK5VA>+J0GP20VpxkO>1H-BJg zXfYplTNRr#WshYxpWVLwoM8C*o1TT?C0`o?g5SR_`JH1gVGkUD%zHT`dyr+R;>pfM zWxKGvD3%8?PqL02SWS|JVOk{{L)mavpfJdTvL+8X-D;v+s$EE{iv$_JdfVAe9HlAI z^s(_RH%w#hh24F9y9rs++yX3b8$FzJm_3dr$H2D2j;@}C zV)IbQ)ns?8j!gzkY3I4w?rvw&-e7VJ_3aNb@9G}5yoEkiFxEwF(@U;46eSCnuAK#;BQd{3eo7_dMLPSJuUhpnz zQL<2VGuPY2UE9uX-gv{7@#85fV1PQRBU=Mh&-SyM^PM~>0{Qvv=Qge0L@61RMRlXl zw0Sm3@l@&&;9>sAqK zA|`HB10jw-HHM~B3Y)gwxUqcg=Gm(zx@T_LURdbqSttx_@9NrK;YuU&=wYAPl=L0G zV@HNp-f3$vUvBX`oF>!drjTLkhPkcRPnEBoo4;}6Lf4M`z_zZgZ3FooU5{M0>&(va zgeC0t7|faNOSkSm&KJ!-=ldN7vn62rfYqe!vf8EkQ2+A->>}!gltcI}?U0XyC#j}p zfX$URMYz!f6BwQk+#hxVsSG$qCX-4b`tf<3CNwHb78oQG%C>9^RrGgvwhUwk8gX0@ zaqebW)lY|PCRc}QYQpR}xBGfg;hzxO`Pg%8y>c|@_IbTNcX0U)&rb!Cmp2Igu(3Sp zqv9q68x^BsGM*<%Z-PpotPKSn!QrG1DAp!8oSeIQF@BL5_1t2d@85NP)5e>&ts|RL z8vl|6A$vwb=XdJRLK;cv0roc`KEr?i2x@MznP{>A+pBudb#tdtPKIy8`AWq^U z?5l@ytKnONLBxACEmNbxG}TDs3|j-rzFWXn%kI{-L3L{emxnAL0aTAn|3*E2D9EN!obLOIi(I z^O9)%4Lba!7H5$7hh3pPXMiFB_}&A^3N2Dxf@kr)CKc6NsKj2@M@_;X*s}5ud=^#v zRL4Z&dxH620fVRr$~nnrD6PmdHnp7MK0NVBk{;YDo;~mf;>Tn7`%_9rujMbzE0SnA z1DCB^%l~>(O!x;P`&Xf(MgmdH9Z%w|B$>764;E-e28^}{OwzZ`sKY;CLl2bc9e5e5 z-YNaC`VLMx^At%%?MV|q1I?mpo$-EfoeifLTIdYBto5nZ|9}QdqOfPsuq_gHphg24 zt0mIn^VpHNLY&0LZDhS@2kit?Gwy{=}0J(5ycT$tnxKn@Dnt=gg>MA7S~tzZs;LggM{E>)15Q*?Tu4em}%3rgqpP1kYyyHwcD#?gn`3C}6 zp7%$LcGbm|(aIm3Y3%0#WXLcxU@2%L!0l;xeK`+`#)Trb4>n)Kr9pt z-Y7eWE%g<|${w*Gfo5%0i)H)&Ukl@(p@mh7s}_bmXEXQ_1%)Nxo)91(*Gd#YoqB@zfk8p9D^FdgmLSy0-6>oo83-vFahBZ zhjd9HNGQw@$Y=!+RP%u7QJs8|uO!eoou?Pa7t|hqY^qS4-BvnM9$xH^E>DxV!9P9` zh~kY+V|?J!k>SO@2#@)B)8zEl(a9sFfkV@k?~&bARe|DfygN5iK3siQ#(SXAfu}Hb zbV*{V31*cn^2eg9_5O%ri&B4qpM-!A&L#jftmy9}+ZK(8JZ)V@KMl{dG>>K{4iAl-+4U!}uulsS zJnW}4gwjSKJ)aeHKFDy6O&=%QNRCq#jU2ZV2K4c1^ba9QKd{m%Fw@U2e9oJY#`<}`7AvbaD{BY> zg%F2m8YrNJ(S);be&V3dXm>edW?NrwY^Hs#w`aChkOfGiaj`gi^>}IND}x1h#9(x~ z!=ao1;yr!yote>`Mil~K;pSFUlD+4eH*D4ML;jFv+l^kYN0Q(VxuCN|^`t!PFb?0L zS}XwvoEj5FLhdF{6ZYE0UBu(7!Z7PZVfd~cXJ=t0@3>{t>@7QmDLlBwAV!hI!bO%Z z;_(q94O`Zb1pH^GM1W%T-9IsivG!9Q%8^0(&z$PwOKb2K>Ymg5E z5;peVRw0p}Ax~WU{?)4^i&kRkqKtz!UY~KeQMp&b7lZqSzxLoEgYAY$Sj|wiA<5fB^ zt<8~$^_kSMvV2Cch9#_Smb>v}(f7$fi&I%?<)_Lkz!iQGk%Qa}Krqy*{iUIg@RX?b z7p2%Z`K}!WkZ(#ibpqbFsMUrmnr)#1(Q2eF&>U<=tu-o<$tl>E&gupHep$hf3hP?t zVIB6m422F`TKGs}vcc(cS!SY@PbRiE{OMVE&fntN66YiFveg%{OouA}8s6Su*B#S! z_)uU#Edu*wS5W`Kk zJX_d-@;)^4GQ(Q7niP{&Igg4Flhtg}>hP&J1d&g>LPE)5(6T*^JE%ky*7P3D#CXsEe$MkchgZM7^{E%kbZk*nDX@6ktRsU@gWM zo-gz$b$L*EIWVT>ZhSK8uSZNNZ6h)`6C&^fr>D*urAjv~cLQQ8cr@J%!h#CQK@<7b z1UX)bm z*26ZG%W=C+r3ep<3;076!-j^H*>ja5+mcx@?NpL8Y-LZXM?2q&5wlugrU~Kzc?^m%bgWG0`E1l!v?I4 zGAX4%EOi`NvyDiT&7a*I{bOC$g*G7Ana4ZG1>$EVzj_1q=L!Gd+<#NrpxYDd5^4L< zF=<%>S+C(`vPf7sIQ0e)$Cpao!EiVjLLtcRu`5cMsMD#a*64Uo<-HhsJQxmzDnr4r zuvTd9Lc44edU}vOEUh^)PF5a`o75o@nfAcP!AkYf>7Uk~LApZ1PMofdlBSTOVeM1RIhFDv2;4Yz zL)wYb@@Bh#YtO_4AK!NL(&5Vnrj9n+6}Pk185wQu9S;l@=k~Sa4;tDtt`@t~;0z|) zlgTcpZ^ux6L#iX|O4_VuM>LRW@->C6&YrPc&t~eCCDtHthV%U*H&Lw;$J#{Hkj81%D5esYAd!eOsQwvYSe%kosR0?aNrQg{PFeIv zONYjW_xD9m^WD+W*uQ@yHPapNxqbdrqB|dNYmMWbv4Q@LaF7(=@PUDzUHNoxOLHic z&32FE@;kdD9f+j3{H}aQV^3>aUnoul`jb|R*&R!1E=tUss-^uhkAH{ zBKh>T2Zk-xP;`nyC*zED=6+meeC3#Hh7UKF{WN_E6-juO;aTHHQ93 zar|G<59H$c_e7h$rq1E0-@hi7UtI%bFZzyoTroRC@0u67(Q@5A~i%bBTVIkk|g7F58po`1Cy zw6LGh55u!9o8x4N%ve-@_8cexClLnjRc`{%T#COa^71J}qlG=W7q$*TfY-3k5&|4J zGfuqbht#Scot`9`*uL^7Wt-A0Jodv1yMe0Hjl1@+c^c2t{5wMxZjBJ+`VDlkB6L}TI`X5glyHAbg!+(-HN$p?rg zpgfILH(hhw$BzsdLGGH*E@JayO+qq}YN4#@Z-g|L<0Nh5_%Kc)5oY8#NqIS5_2t*X z(|MN~JV|;9KcRKp5b%`86yqi}FS4H7QGE#v>(Cq6f3N$Lf|d=H-%9at`SfwB&(dc3 zIpuHk@hJs;1gqm>wWrstwndTfa=iX~KPNmRIbQ$0pA(*u9AEvtvj63_y;Zlv1dPAwP(+aZzI~ z%Er;J0gP0w8;?QNgIBoOfJVe9Mp4jZMV6PZhI@|sa>#y~Ib_ds*O!0Y6*j)X?2dZE z=9e|yYzX_WBq)z&kHu*bnam1y<_DyR7aF2B!Rc`3iGwIyB| zMK25oe^uoMhX*?#Rw^j$MoD4ZDRw}P(_YK*lEw!TGsFip4n7do2@;yPMnV_ZKj2-J zA$y}3fJZNS%(YNdC=m&(Q0ZGK(vPe%6pz?IKiajWlRiXi;mLZrS34!tqh<*qO*pSc zfjG#`FrX6&=C;CyR60Th33pzf~|2K~BjK9m`vH1hs zVYYrX^`t8uc6~AQ15YXtO1r-`^Y2J%l%BGhBN zurZq={DmB~m<`c+1s~;8Gztrv{>f@@^YWrE*Y2x4VY>ujt?xHDyq=`xTMfov7`?5^ zN3uBnx4bEiqX#xG|EM=Ni8ktqe2GB(Y(mK zX>-?LQ31{y>z+Y-BB2Y5`ZI(ipi#3r&R$-F7CbDW#}pk}_~mkRbbxks2+W9XXZXv3 z3?}lIDx7KTC%%V{+DJL<8E)1Z?JK)dM z{<>=eKR8~wqoac#?C7YxN&XHD@Xr)0Egkree}R4idtx2}dv|KsdsM7Nj+4x%aagJQ zXf1Nq%lhcREi?*hSN=zIQd*&BuoeCy@eGWs7erR$)ill<6jVP-c=dO6<4rUUOwe<} zst+d*T9FyJO-hXpoZ`dT6!UrD1CmU5t7dZvbyo(1@gVHm(!AAVkt`6ShI$$_+PoW3 zOCp;A)IO%eE|#0x+mK*meLZc1?Sq+QtTmqVdxa&50}VDHx6YD;XPZG2hxO|&e5-Wx z1O8a?B})jW5BQs!n#0+0F7{$FZI|G)Fr;VQ=Hs4HY|ja_LnW zl}V^#2;C+O`(W4lqUTAe&Xb~*UgJkqjZje3-3ToxA;2%^2 zaE4>Bf3N+Z9fs`O+=X_;3sL7R>1SFNvsr|(HiP2=WSD8M$aIS{BN_$l!?+k!kb0ML>BYjuL2&l0O*2VT@uPc>8(jz`($P($Il? zcW|!QGd9-KJwE<~Kcu*)qx_Y4N4|e~*T{jf9{KjcnE%ahhAX6Cg|F0Ci0T|#ncfp= zx-LyHLrd{PlEC0!y9BiUrsj6yE(4iBSUYtY;nb&IoAJQnkT&7EnPR^BOo_kk;A7k_ zfL_7S0r>VK`0;n3-3Y~RMYmw;uOf(%nv{oR`jTwpFqH;0r>Xgc=oXCAEM~LLh~l0F zxOAd9xr{Z{HSZQZyZPUGqfu|=&Yx^PQ%2wJpZrAj?fyxvU&W7xj!jLYVRvS-r+Xra z+0yLLUmUZIW42tpK7`rIK6YRjIY;s}#q`9)pkEE^ObqTY8lIt@##<>-R(VaezZp*5 zF5lvhYu|Q=&eCvsvUU&^Iv02SJEs0Kv;xIG&~@&1qCYtas<9T6@Owq4iVzPO>!+bR z4nUGVmow-HN@rs4)@E~_Uj1DlMl-_hL{@gF!&tACr5Uo!Xcev+sTPz@f^qdcdXl0j z^bVi~`=qMIDi#%*?6QzM$udTeQX+Mh9-!=iM8P9>Ug4+I1vO@)pef#4BAC=^oo z;~#eM7_NnI@c4Y?8F%FkG!f-7`ia@RC7NiJH$!BVo1Xc{Sl~f=L;N_8DQmbx&uQ#b zPUsQZxw{M$GkKHn^q!@YFm;yFAoA4{`SOG&R@kz#g2Y-(Gcj+-QsESM{I-x*fc&ET+}*)<5*@3 zX$G{Fue$sBKnB}F)g&lA>8G%i3a56!rP}}S9XpPOx7oR^@*R68|Dr$swztu~z3pu= z{&`22t@17Iw6{4Ns_lEv*j2kL>E@exx4X&ZbXQ(exy?mqxj4=ti9WBDZ|tEKuP9S- z@mu*dZl|lsUHNk5!|o(@wTO-nd(efa%x;nAsybl8;C7TVsQy(<*8nHzqNd@8b3PhtfdjU1%6uR3`IK9WOx?CxH;@H^WuAV&`C%vJNcc8d=@5cVU zjjl98F1GAVdp2J^(L6K}Ynq$sn!Uvu%V#_ptJUmIb!KO}C+EXB;WjfmHrkT)*=^Re zYhb#tSn8bak0yAie|u?4_9h@YtAImf|3Mp^64`|_G%vbv3WlWSh$xYx{)2T43HnDD zP6~x!+i7tg5XI~Li3AFV_z#^yqHvsyjG0BpOM12ZkIGR`wNtM$z5G|^295G71{3J` zErQZdBc@R&HFOB#1qo4LtNOZ<6h?gT-Gb6ntgqZ{M=KoJK?N;mjG_T?XdV z#E9xz7hFV8qbr5=QrAVuCP}cg4w|Kqk8c+%t;$F3^f&02{G^XyH{^+Fh-I#_^Focl zP13|;?V=iOH!i-YW#TC@&|MH;`?bxr-25j&n9Hg<+|+o zqj)_fUW@*3i0YcCW0s~xzK&Uj82TO51bA(V@FtyTezaDC*O7|e)mfpq4n$;SZdJ!* zAn(rQn{>&DPDl9mS5#-C&>aiti7Z~bfHUgKV4{8xOj-Hm%I}m1)fgh32{!i+3C_37 zU9iKHumg{X5^KCZcfq-841^wA4T1(jHqPu;N@l5``+aH2uCipe>g}0|91=0aKhfRc zvDj?Qev7XmJ`&`9e{0TP`B`RZvGQ=RH5cL|4RL$Mxr$uv1;YypLtzuWT)E9>{mJqh4yT(AJ7}L5k39 z5OJqq;lbtr&7l3c5#k*VMcA;drI|Xr^>#N8wG1U<{!Abv+eVViI(ktbH|^#S?W`J1 zJe(Bu}e>=CC%AKCTFBgNv8SMJ*Ns-wl?(O2!-b$-j1^SgH4x_R@h=rd|CP$LV2 zA-cb7=ZmJMuHMNDEv3{yCqD(+_kra^>D%`L{5lJkyRyR}^gTyi?$$+n}p)Llw z+Rx#qE`Rw(ntAKu=JA8Y9zK1;N6G5J^tiI){$uwO(cZuH>XF8Q^zbG5N~Ox|q5&xE zEZ7|rm}ipCnPVe3y950Mln{(q&EA2kr1qmj>>A_& zLF;=Pf#}zzpYiptOF!f5z|+Iz&sP38dU7Mm!uGe!^z~1rXfBY!Y5{Xu>ji1&`PcV? z99Gc1<^?Sg$?FypOp5j9#RmiN&-x+}AG+0l4K4A?*KYaR*Lr5#+GcychRNL?@%bXP z=`A+P>4^?&>TDHL6pzA;1+FN%Ad$6XF;dehpevOWz16x>UG-=q30cn(>rStZZT%+U zvIU|KZ9X$Gac1*F4@o3Gr0k(>hAdfy%}rO0YtyZ~L?=gL8O#(mfl3{s1BjNwhQ}pX ziV=ZkyaVr-Q+qfp0Wi1;%V066YFOPC3Nn^M3KW#s9%{!Y{i$pU!OV`OI-fE}trN)J zr0cIPwjq=|tygq!YHQon{a?-8BuQcU*s~cH8Z9hH%2>zD5cF!e73;{NQwXgC?Vd~q z3p{0JLUog7q>0FlQ4;|oewE9lo6zK0>$QTCLhG8X&}EXoCxX++!e|j0rUArqbZPY= z5}EIn#Pcnf9b2MG&wC{ey|;>aTq)66Pw2R?L|iMOEfN-!)>#gE>29HyU?GuBf(rK{ z8!wOaBQygqlYzdB3|mBZJU~Xk-Jn_&ipnGu)aEnR*`7|~KylGp#7E63i#gP>P|%uL zsJcQXFXFkb!+H@vOkK*{FZ5C@zO;wVOI2R~0x!oRDxAU!pTG*cu|m50$w=7@dHqUt zZDd|ipaZT{Zx8Am26~FU#cXpp2*(Ild|9w>P;W4zpnhR%2lIMkYLnR{9Hzk{Meb!U zG8NoJZgzlAZ@i^oF$Ghuj>`(w8@|d5Syw6qBgZmy?cA=rPK}J7ynBg|zzq?)$DDz$#cGyp0 zeLeS06WT2c*#F(cdP4s7g0{&n8K5qA)uu9dk4~SSgr+kYvGc^NU{fh-T61Kj^mOJ1 zTJxxDUsdBwN{)mg;#3rgtgH40%hnqRFbOnFLDfN>s5}tM?w%>`>+ddKHFMzHR21P? z)U`wGJVUZmiO|oO|Ni-_H0y=z83?4R7KGTLerg2ei(vk^xx{c7JMTjjVTj!%Bi9JK zGBlz?pv!EQ=aS;{v_`hnpwQ}sJKe4WsVZa?K|(vl5zbYVum-OA-iXoWvSgh0^<;-~ zLo)v0Zwa(W1C_9`wy(K}KWO??i#_NBCGU(={&V-MkCsYDU%h+JJtu~SPTaG6h19X% z{&rLuEMK{!vvbFl<%yH?9Ub#0FKX7QH+9z>xcRkR(19DWM~@5H+{oJ4%LHyd6pP|= z0=_jyf8YipI@dx6CljbVfa!&62~N@pYs!JZFgIvwhYAz`%#||$gJd^CY+MY50)=D~ z9iq&nn%a_W!NyRd+kqa;1}YFC!6-;3Qy?Rk%0wU%wbKDbUkJUYaKWJ$Z?1Tj^;Gd;b2ik68SV{mZ|I_Or3TigoDNYcJNJY!x(*+u($PqUwLFI3jWz;Z<>$w$@}5yppcg zLfb%drYV=q(d8sKLe&WGRSFGZu-E(%bG4m6q9%e2s9E4@HsJq;+OJ@KS6|m$cXq=_ zW9KC!^Ve_a-hQa^|NOCou`dyM^JV&pn)Xn*c=>Q~Z?C5*cu68NbIXpYt9Ep`>bnc- z+(6#ux3OLaB8puk>#Kz%I6Kd<5hhB?fY>4E4^mZ0SYCvO105N%_MK^0N|uw5MX|7o zb2(mB&3;{%)hX;TN(k`xiRkNZaET*WdaV_t%=rC2@&7V{jO&|g3d^iKqdtH&6p_S) zEYENxz;Ou(s1Hp5h&V+(P-tGE%QS=qIXpCwPelDbn-!#n*61Y5iPSuhOlA?t8Z6=} z;Gzf*irA=#r8FwC$u8n6)D)FA$(Q2aMzUn4-~UoumjmVfN1~Gph0oYjyCq~(T#kUn zuG&6V*f|wF)?jos#G|H{1_q}4V(yPsZlfrmuRHf+7k6NDK3|wfnz+Z+1 zTxmwZa4c5qOed-aP(A=zi-l8XH@@ign<_uE`_Z88kHIHG<<3}3%olBPe%l;!xfIc& z?!`)EV$Ndp`tdPKF}wUJFP)@X`5CAQ@pweGC}M6>x4e^jRieX|IP6MstzRm)VW+&T zm$XU?x-~STV<`N5J$4UD6KBXH$o62jDBmDBEuq4}Y|GKjEq-4$u21_`HDfh1gX|=7 zmPnH799GX@tAqZZ23v*tccHV&K-*$x$E71%Upxh_((XFZasgGADlfE!L;C^|jiU(f zEh{?=&4|?DX4>+WLQGLmh>a7haF82N9KjWvSvhXCC?*ql$Y$e9!Z{RfRcR3CqB`4h z&6!j(81NyRX>9;Cj`1-ERwW$g46F#1$V^y(8R#P_o2&+{A^fW9!dDGYlYqpQsK4>Y zyjHL2M<-yL$4?;5)9I)ST>tC{>CYoiYeDL#vhW%&#Qf*E*}``bkr8E9-cNK{AG?0E z8nNb^s$pteulk_Kbrs=DQl#3#HnYF{5PEl`d_?P7)W@I|43;wpMyuv)&>t1ZfpDQ4 zVJVs;b{nQ7r8})~U=nDajZldG8EBMqFXTheCDeR%YO*|08tiP(W~kh)VY78JwPCHw z-9i!A!PVa|+t4$c3{j0r{)})oJf4Hm-2Ww}O&)L-S&|3;kFjmw_|u@Zc2+Krr8((h zm>Yym+)D>H>NwIM2F);`f)hbz)InF?2PDV~-50coK5htx9OF89RcQ&m0A;d6qa_8Y zp9WjRT&Y}5flcx^PUA{-8908LZak!Aag()5I@K^S-=xQnKOsXmWSh#!HYCK`!9Hg7 zD%=ZxP*(iym!}(CIG=bDtF%B=x3j78M8Kh#NuZn1$QEp zN;AyP_Gr5s{eq!yvF~IdD|e} z4X}KR#`a);%w|H+n)CoE(}vs{F|~mzUIfW$CnmtSqV2t8Tw=g_ACO!0AfRNt7kne~ zKy9D947x6y+ z@bdr7%azwwKJq-b zXz)w-?yWrV0`4KxvVDXjnKK(>TLlL6j@lXoVL(FUwcC>Qd%m>18|9ZJM3H?)K11IL zDY57oQex%NyOZ#&KZ7zpHA|kfmR4VmJyd7K4x#T2crwrAy{0uf&^;4KA>ueQK7sz# z;J^#wwK#98^(adww_$@Wtg1@a4qGhGTm{q4B%I7&NI8H%HLw(M`$ zpX!(W?W`8;9@@idR!ugSueK48g;6@AP=!8~(xPny>^``8PQ`5IyU==)cTZe3yZPGk z#v8YZc6I}Wh3@W!|KR);cfH@2Fq`>ZbzW&u)H61YDwPJ~IdsNF)g#?K2VK*lZQ>! zV3{JaMe;978;M{VjwUKNz7u_XaCcA7?!m_&Z!Z+uA3t+Nyt%FE_rLGi)V^8~aK- z0-mm9syP^OG&mh8S6XOXu>w&``wOAV2StBK^ftiq{!@8!or#bec9p#~S4)I4_W=d_)d?XJlnm1L7bCejE-0#j7m1PFGt7`Jd?& z)rOJ(X)%M5a6nIYl`_}@@;8gxS-p_Hz`WR^pxF%n38C&>PUQUV@JF})wYAOeR2uHT z-HH;ZR_m*F&^ebKud=q<(S7#z`>YBIqpW|uHHwZLAN#I@+ntThCm(k;Ic(hh)kg?l zk9^h5ZH^|_<4@u(J9m8dWAap4$I9>d4{;)MjJ^1g0u=*5EB8a+wqW&&YB~dUTU81L zV#Kl$q1IxzSqX-JR)VJQDCC(hq-Ji}YtTJP4l=G>lA+2s7kYQM8Qt z%S@EHpfM$Pcki4EI&n}j=Lm1<9@?1C76b8kpqOqME_6)?ooP2rujI+m(Mt-=W6kZ; zt)(SPpe5+c;s!HEu&K8-)2RgA?f~ydrdy+7muhRsxSG4$rrXnlO}rsJlI@-AB#M#e zcA7y^OM+7537j}2{qF^(oIa*Q3A&ZSSx@{XcdPiNXHpC+w9Ibj?XHdhT2l!4aweR&Ye!k#aooWqT@u z!eVh0>Y=?L0<2BUPVnrX#H(EMeK!|iXhexmOQOUmbUSia&^(kRs{c@yS_1|R=WWs4Wqal*?V zr?WC7)jtWTo~HWPrp8DZF+|FaK{}v{{X9bA5aTM9A&O8VDojyH2cezjka@Lm748EN z7X|3mW{78FYIF~;5FCyEuRQ5*3`P)SjYd8Z90+{8+P5uH`8VF5fM`MYHb|9omda0? zaJ0u~sk}T8t?He6Z>uzo;nxd{TA%Mvhw{Ew-Q1ht_WiFqKj3vr4)rc zW3>IK5-=krAEA)vK9Q@Fl8dyAMyQlUAjIn@Tz2|%KEK^=PkL>BTWTc!D@(@Xwl@4G zbL#EY(=JU7|IuLjrOjr?J4UO?RGv_BK1YKo?O*=Jwk6b})}4Hr^@=;$#avbJp<=|` zP3mbZnYuX!l~2Hn^Pv8zk8LbZvW5m!_FN9s7Ww#rEu4f5P2s+`0rT+t=U=dCu)Ch2>W)}X6bG7<_n zXGvk+Y6ukTFl@S_huRiQY#EvBK|HFs%SQ}=A^gY~3|`E7@- z`#y!vjrOva$aAAx!!twQ6o(&9i% zu)f|%m@|aF$NI)PJKA!orf^TB$L|xzYEfUQg}!Ucl_hPo+i-F(9)^wIQimL9m}6-8 zML1|2%#>z3c<(~x#^L>W!W50nwD&CVZ)${LLN74hB(U+lj?u=sq4FVW09HRVQa(~L zts)nJu^sBepclf}EPI{ASdOqDNXE(0BJB>VM4-)tpbIhps9)RY~UgaY&kaJt}(v*i^>-5no3u zA-E?P4ZT2|zGod$gl^5Qoa7bI;+l#yAtd+}Na+3FE7H7y=smRWIG7ZVd#Qrp?&uq0 z{u9&)j>ape_+N_OBPgRIo0F`e-{G3!3@Xy#nU?8J(rQH-tRUf&QX4-6ZKyp_JQ_#_ zlTK>shLgF~kWIl;_0Kd*P&;J=V<;HpTQ^@dsziPM*gGeW4Ub&8WpeM_=sUAh4TUQv zd8;1>t6ti*XL#t6{NAC`;NT@KBWcnZe$46?F)Q@SWRvCb>b$5`fIMLaD_=}cWYDMt zj3$_CNw%h1G0mo6Q+1lU@{ECUg!==OJ zp-pqeS8S<#gQ|-@&@i+##&hI5-qf~xV(dU4^aG|{+QM%Q`+VWOnEL@#-F!itdsp?W zaiY2aDP`r<8$|t@D7!#9vsT_l33rmMwFOeptelW*Y|SQ7==LmYe2%Rk`q#O|#7C!& z4GTZ{uJJ1dONS?iHcuDs+QQ#Vq5}V{#Y5w?KheNTJ9m$cALzwGO9Ph-ZJu8(DHI0F z6vqzIFh(*I<%dcqOR=7ggv(511kpfc5Q#XtaX1;$v}7#82!m+?6Gd99*qu;l?G`D0 znbuIlTOMjiNZYj$7VX4?qn8bo4(^$sp6tKP8uIhwm9x4H>vX@WcXz3{w{KtfP#@iD zh+R?Nf?c6XHt0Vp?r26j!op1`XG7o&dCcQf=o>+e3!Own5JU5-TGt0dFo5b|2o~XJ z;t=dmYmRz&<@#FtlEUNkx@`?6BSQt8=5ay<(ZQoSWfw@v!id_PzIE)QJXSt3RQWOz z8r0i0%DWXZNAIPDADJB9JlTKmENxWgG1@hGK=Yx8#in8P=xnlYJl{8hO(Tm%>tO?n zh3cQ=EfI8gh6BDdRflmn=-4E^@(lv1TKT3GM6|c2lL(~eU3pm!K~~F4G|mBIVOi*r zE$uLDqVz*V{FVekHRu6n`W3`VKkJCMW#d;qx7$bme*4_lZP1EF5Lt*~MHC%a zuij1bmnjedO3Y=kR~^}Wt{)esdvRs}7lcSJxZVfWLTlzVfgH-O3W*`t&COSBau{6p zq$@bwTwLhx+FsZ;;7B@fbRc(RblXkSv$wp6)+-im*kDgNtcu<5?cZNQpPC_usllFf zc6ZO-x&wD*{I{+#2vx%edP}UysQ4tfd3JW{`AdOZ#9-DXsqtK)9a8JYHmX&R1 z=PtxS?Kp`H=M7y=nHfbss@Ze`+bgmrs#fGvEpdyLa~H#EHF4rIdNCX-z@D!j4mRJP zUk|5@`i0H6skMLjD5{ypqm?b1J##zJ4nHY=#{A!tcHiWo5-dT{@l?4$tgyVpA5#|L zeWItlFGRNw;bt3ko1pd>BIcqeEZyOS1MjG8LwMzch) zf9UCFZS6^!3d@}T^_*=UmkWGU4_O!;)~v2*xA*; zIP?KX$4KR6F2#EOrSErlz5it|>zJ5`23}=vpX+{ZSiO@?B5)(e&eA2Rpio z6i-C&9p!HD%9yMHhPApC)o;k1pibo^x|1ksgF@w1;1u#sMZgVA2G4sCg>RYWOg86p zTg^?Pm}MZZ`K9EAR>gcZf>w2WpxfxJOtqscT{ivNP#Emr(X*p~l5fsBoxn#rDQ<<^c)5H}@I}tisyhQ_fBKP++x9)r0rI-HY zRina^`mM47>k}sq=$w8Rs2<07bu$n31u!;5`=xr5N?0WsMy;1{1Ivk3A9xnKqy=_K z=X2X7`VoOT9j8j#*K+&(*yNERVSl}5u znY7#``Kp%V@2GLSf{MA{Dc=Si_d*-MBTr!45i3S?2+l5t7({12y0;MYTeVE`{au}K zDh;fc_iC0&p&*OpvQ$e$)pF1cbd1<+uCJ4W0fQ}7iz$BIA0ON7n>5Cgc9SXZ8{g6W zCKoQ7{@a+#`9RmUQU8FEn{COsaniSCJnFfvvK;n(PHk;_qt$|TmSM-sI(tLLkNZ;@ z?_Npe@+wy44hF(KDY~Bpcb6U+-qcxwcJmhwf=JjJD-v_SgoDF#q2y&7N5w-*s&5|#n*HtEBA-^-} zN=hSwya>&R&`1DAgbnUd$;@9xUBquBaZ=cAyRq(Q!CA$i2aLpia&x^QOifME*jA(;eniT$Si)?4fhC zD6^qE%rQ~YW)v@6r|7Qcq}R=x&(Or%$6M%D@`YI9V9)f4(Xo@2mHI{e2N~S?ONJ4U zYD!!dO3d7{Ywyc8VU!HI+KMzJJ5OAm~s`@EU6bl_~ zE$K#7Ei}wnXR5_l!p5l;-4S;oJwO4xKQDn|@#Xj^?|bj`UheeXXLq)@*{Yr0)pqr26-$;aS(b$@Tec;+A{%5{Z1Mqdqu8be zOaL2VV;oEj!Nds=Cy)dugakrph8PG0i0z&I|IYi~nVns&EWwxWdH#On*(rDCzU929 z{?0k@49ow?jJEGPoTa&~+-(tWDoiD6K{^ zDK+e{dW?_m3HwEGZBhx0{?PCKptV{IY zCZEZo{0^+g1z-x{V7PEhVK>JVzV`fh!NjN8n^(;02tp^|ZHRoaZ&10!P1XJjiGXdA z|5VN(n#Zd*DOW;kCpm|hl(G^0e03}##Q23`31M-Ahc|F(&hf}Bf0JfrLy2LUgu(Eu z|FL9TKi0sJ!@e)T?gnRCgA+uSv=C0?jRdRjVpqaYncKsdI#hKnjo(n}E<@J6LT7lx-danZN z?F+N{+kCw^#)E%XU^kBLp%eT&bl1aTDZtri&Rt%EX?b@&p^R~Y0yeO`3@^xa7i#bc z^5O>c|5kTB%wOP7X#Ro{y0z}i0=q%)|AwztcY#(*AQh;`l|UwFRWx}GSGrcbE9#Ec zR~E8~<6SNz3e&WGJfr#%t0Db_+A301;i&Veg!B`YBJ5V3SMSAl1=8;%e2)ztx>bD( zm#`oG`W#&Xel|7r%X51UaL`1o8ne~nzr5a{_AX|ty~`^cigyW}p}kwajn+#pXERl@q0 zRirjh6;7?!)k*`4yOe4&Z-wQ7QE`+q$2Wr6U zmq}+@0*7b+uY5h@`-GVI5g6s*4_e+^?I+)fkU3uKc4Nu(Smw>&Q^m!rUg z4w;xbU0$~#7Ik1RvZ-iOtmue1U@&O8m>6y?>qT?IbET=Q?Ti-E(_S4S3X=zDGpc@P zB7#n<^iDOf^uGKFxZB`hK>e$avXI{sh4WO6RzJ!sKj0IZ-4gEi5MNuT@cm6u z#To>!;JYLvmrC+1{c4tT(5M&%UqJiq$ew`;i2v>K%&=-batSCFd6kO?w>cyviX|2 z&WR#P5@9VAlcXFW07i>x1F{mxshO6{viZzwUs-A97vYPF>CR=BVzqXWwxJy0KQAep zk)@DqZK1LpNVOYl9i!-1BIYjzN*FX1=dhbXr9*^27BM1TxQ5GAV}??Iu($vOYIr0( zg;#&IVocwS@49Vl^wzzZ?5Pj$*!F>w`%Zpv>y8iIn9c0HwK9I|?zlyb>U5OKh_Xz* z8^&(fFn@Al!>$J|*?-S2oo?4X`!BhF_lAj+^BZm$+psC(>&(erkvXE8t>J}#i;t~R zkxA5683fgO;ot9-1}kNF>&%Ktk&&Ktr4boWARkHu&l@&06@BpSb+jSRyEmVOR#EU3 z@I6l-f{h!>)$a& z>2{$q@=9bK9Y>7**{$h;x#DkW9>3lEhIQ-Suvt7W&JCouKHFIKfMnk&wyh0$nIq)q z4Vri+KUj!Nfb9}@Ti_%ZpjPpt(9xDoL_%&Syn29F8?|Fe_DQd{ZJO3-^wrE0+PR)S zxPOJVK;cX}FJ#G|pfP@Q=3vjBkKAzmM|byJHuF)M!`IY^Lml0ieb<>Y@5*i*RqYWq zgp#ni@V;ATj=pE>*7qEpxpg02s_>opE_eA%;ko_gQ*V3Qsq+50u*v7TT=1Kk+|A1b zNjE4LFFdLB+4?h)Rka8{B$F)6%fJ)Dwz+t*dy%f8oUX=eNFL^8=nm(;hRc$?yz!jZ zd;l@@J=7^g-mYD9Ex#A_WUbw+RXgW-nc&(OPxY0v#`96JU-%u=uH)l5dDb9LcWuR0xD0~BJZXG5udV`tmio}wr3Zy(ykl4;xi@#8Y{A1&`R_}wZB zYsk8(TeqygZT{x#+qUgY_U`H}@9Hj2Ueta`=eE%dOTOG){W+p+YWQ25zwLCfu#c*d zkpJE9+{f3Fm;&XM}#^kP&y(5FLonVmsNU(L*!&#K^9JCrm~t>Tg!VIEFv^ z=lQ{w^#}VXlD@6Cmm=x9(z?s~JNM4DD<3xc%qmLnsotk5Cr8FEpUI(1Wq&{QXYcRd z*@dC60}iDdXLW*dRuN66vZgXxXlD0~TUXbE#bcmgh@h6Z}N z+DZk$XoCUd7frAUqFwMLnpBxTatan|8;BLt)pU(o>tYciCju6p5Rf^ssje-Tw-wr( zGu@YO9N0DHu^NJ2y~h;YJlZsx9dErx4XA zhvJ99Oy6eY1W8o_9%Itv!XT-)M@ICJlV9e80qIA{>~KA@}KuJ7Oe4)%Ke0P+#H4-8)U_U&)|_~y+Yf6MkiJTkPU>xP2|Z|GwC58Tkx za|7s!bdx!FIcgofi?sC80m_Bo$EMcTiu??OD_qh${k3DkI=8l+*}1c4kRDyhY%=>Ya2Fxnt<{Z%$R(DuXSX`X1#B>`q_U z5C~oLrgf9!N66Y39ogB!*zWyPn=`C9v&75NC<=Ax%I4V~hi7A7aju;aNAvj1evfQQ z4Rv)6QJtK`K-1bmoyOTf>6(8T6K%n~7%*Rw{Es+cMdH}N#UW%tC>f9xNoXEpWZI~||NXi=h4&djcC&`x*DDvuT*6j`^0>7If!pPNwFTfn04B@$o{(&NWEb7^-l9^_=O zk~T1k-LQ?;fg2{IY$a7^fFetq6RT$Uzyct)%(bW*Hi26_&@nW%7sP2`TW7CD+L0P5 zOtrO76$sTbdX2t(^(Lk#7WV4Ed@eUX@G56#uWY)y%y-f+zl=_e4LNS`MG@9t?zMT=D5w5_YCy!?Pb;Hk>hqTG2~Z20A`GJ%|go1?q*5mq1P6|x)3yA36L-_ z#JGyXq+}diz$&3;4=pZ9j2`kW@&#=O^$JrmgHCRWVXGR^fBMsUgn4cE-eZg5)h9k9 zD-mDF`R;eyp|jX6Y>ZVbsDT-%{)*WGQPpOtK3DyW-ipQqDD7T-5nee$fcWLFV%0qo zA-)kA0+_>i28pT|AA$Ph0}7BHavlgrgQ3{~#J2==J`msC6nj_U#**B5w*`)@N%HN~ zNx(toAIyjb8VPi-NGSYZq;*p_kw$6V!NIA)L+eXKFWj%f*g=|&@WB0NzGM(<%Zb~E1jy&3e$$0uG*Z))X_{?sM0V9AaxXB_X4{Jt01=K zl5!xOG}@yszei_jD}~DK*>VEtjbNgjZR-z~TCIx5?~2-u$yBg>$(qjbW{0DBTt9Ht zrs4flB{O0GER?hlUN_Y=Q%a7se1r(;BP}Dz(oECTb%XX~2>uVJxiqzZc+*t_(T+r- zBTC$yYjnzXcyX?U{4pqdDQ>#>O)SS8;oP-%yZ?~hh2Mx>i9msP*Qo4Uyp+d#`1$9* zLAjVlWv}$#OYievq4yC@+$ZD*xCQ?e@$SA3dY8*V{u?K?-$3LV@AEz3@82%oXBa2H z9{w0*7she~qoRBM1{BFVn9BlFz^y?3&8xIu{10tZuqF8W$F%q1>*8E2Bv+8>+e&gQ zA9p$+3_t*i!qqvbAe;z5)-Wun487_BM{Q-T8cV}YWB=ix1ORJ=yULfteuFNklmTrz zJ3&;}->3F=oE!~_+!Ca1I?`4&d^!W7m z?J`Q{*+2Hl;oTiw^UctXyKvMTu=2SagNt3WX?wYALa^aJ30-QK)F`^Net^U%YR;Im zL>Ei0yRyIk%5~zgqmoFDc65#=5|xWTdGpPmym|&N$Br}P z-~>02SQ5HjKtk<&umd_rGimv>LMev9uzflofa#JrPR!;pL?QPoOZZ# zUGLFbbiNQIfP5@z2&;&Lm1Q}iE^HRn8^*8-A>6ej^5_6ImUXAGX$qxRprtwVr~Sm0 zuoOrF%mQOX-DgUynHGXnryhjf8k`f_Q0i-ARJ2&uaW!IR=~yx~)P%Sm!CywM9G^H; z4i$Vau?+bU!HVo`d$JO3oyav!GM$MRw z=BJ<)6pV++Qj_<>0V8}BXH8K!<5oqH&~i&o7$QmYa&q_FQ>=*3YA zD?|Irs0z_Dkr=yL9(~;e6`E+CZdyOlI$4C88&343+D8+pM2(WG1ABV9cb4-bZ4URy z=C<*}L)a)tYKdz903Dp7{Yvua8BBPP3%S-foshZ9cMBB|fNOHtAOHabrsmX8VR+96 zD#9)(g-;go4;-*uA>|IRzwm~uAM*tj3wxu{Yg2>cGz(OAekj2DFsd`*w1d53wxZb# z3YWV>XVl0~W9=e@+iY|5EL{bK-K9pNUT^eM6W7hmTt5LmNHah&Z_W{l0ujy=fvJmAFUNwzmJlN+ zwvbIXG6rpZUz0Haj)(;Z44<`59vvP&I=S}B;o&PMnYv!?cKmMz%3Xz>uU>&exmnyqwxA=s$Zm)Vs`?^rc^)gjHpJ3S-zAMg@|gf85g#P zalS@?+q9$0Byms5PJdkFT4#_M%`A`s$e*TU8*Z;tTg8U+hsY--F*Qlh#&oE;MBkul zZt#tt;|rB`BsU>!LB(YFcO>l_SDxb=h=L-aU6>yOWlCvpDkR%e?Ts5|TRQS$S86#+ zeRZd#AL^6X&_3+i^3P!dv)nlL@5sdX5tO32V!SX4ZF00&9L;7b#r|F0-Mjknh;j$z zcsmYYd!luiu@N?U)!K2ClQ}ZJ_NvkC%~Pf3@j_v|xirXkIu0$=!wxxB0IV*Y+K+D;$g9Ca@K zn(b!yqWVV;9&|)~yhb>ZNt_A)I4}|z9r*K-u{cbbjgc_HSMr^O&Qv0ti{v0^P$7g^ zGxT#BrEnn&o@EqTfk??I@Wd*iNo4ZmBmZ)7ZBt(=Se)zWnJt9VeN7X^SfLP$6^pSz zJRS%n5|(&>uBB`?rJGv@2T>Z+WGR<&{qf0kD1=y3IF$YihR%FwDDF{_poo#N+u0f} zDK8h${UlOs1#<%!1Y2;Zfytq#Fcr&%yLAO0TzRP;p$-H(@>wJphpDWQaAOf4+$tt5 z3gePEBJH@&w~3#Cc|TSdPWkM1Up5xcHl^llE|=|Nc8|w?N2y~ZwuRlE=+8Jk z9;ZKwwxY zit z$cH@sfO~LmsxRH?{ez6h(kn&5{Ym%Mk5>2(`rx2(h~b7u0hTiO~~;1(JcXwTTLfJ`GnsWCyW8L9ayR|oy=>-H?mQw5{ zb`m=hlX!;J3Vr1WW;4lMy?i!_IBkW~4t)jrUq)=k7*RVPYfZJSgBvj7LU%8-yVVc4 zRO{!As>KydI=x!&&F)jx&1QUriPXWQnB0Zx3lNW2%iPq)m~S^Cx6in0MWu^fGjGo2J}hu z>hdz7vkUS=(ywp_qx@d>{pxK3^-#S{pdQd8^tbYNmtFhU{MrZEmEv0Y$@;aei@#OU zN(8@)dYk-rU)HYu*jd+xP`lw5%YOHB_>NJ&A3T`Gh#n$JVA*f|T>Jd#+UN2w6&LRH zfOfC1YoFiGKab+~zr{a)xAyr{+UK|9FO#|!e~zwc*CXD*{okk%ps}tjtPl z%3UlP#a$OJ6vEMb-i60PAsR-i1pUfR>1q6mUrJP>crCGA+<3%qF=;cCPx zFCRl%@JFx|rpF}q=rLjc#%UD%bF$kF@?(p~@Hy(_)8~+FMUeF^(i<_?0HRJEl~$gv z2HQf1+Fg21Nui(}b>S;D+t&FViib`LEP5PrlI>B&yPg%`A9Th_*%GFh(rs; zzljv`(MSPH7AY2@k-YdzOBg8@BUn6c%Sw-fe^Xy9cwtE1vjbQ?sN3XPCM}>(w1L@10Tc2YPOE4!2rE9Fiz*87};g}~(x6c=zK$$g;XR4aK(I+rM>|7*I^mTAC zApKCz>Zj$8OI>odB0Zy3QI@lsb^<+>y{aGz1?1MT`A6eW|MFzn!oEEXdj(3h{x%F( z(Tq+Q3jopUbbUI=P_*Y>aUp;NP;|+*KyBL*wKbiqaH{v93gFsJUxAP07t8SI>@JK9n0pK^*`I3xx>&M~|n% z#hFmonzqbP7F9V(MV>k`(tlvAfAd%>ur}a}JW;1{tW*}67ca|KL5usOoRqJsuhUh0 zotTtlZPNSMr$Ak+pMj<01o>OvsRP)dNHmvYZG2D|7?xX_XJ$6a7Dyvp^*q5Ia55*I zC;S%u7#&W&9M7ivllYe^r;}x7PW7kgbvcD6^t(m&eYO|0#`{l2p&ukYXph~~Y*dh< ztgO=mvZj;ng!d1BNT-JxLP({K_NHPs9S%D1O;#prNlc;4F%SIafdJfm^;sE><(hzk z3y3z2JBbX2Tl^u5wR0FSrI_2~G4WV1F^|=2 z%>XBol@-J5g+G#?&G{3u8ce> zN&X;Y#{_Ci9jNT1DdwQx$?M!sz!0g5Ck&s%c4oEqu7}BlkN4XwF2q^;A>=qUuU*Te zwbN^-rzR_Fh6c;MT^%jWO@&M<9t{Plx{L*-J|(~=JXoGFNFPY*3|0Q2*=K~@E|-^{ zKulrby2~{+O!$9{#%}S%DdSrCM!3XS!ap6J^7&%n`Lqtitm=|~oYG;{R3$x!iUe{r zuqm|O8;FIrruFcx`E==zK>GN6UVZY7;g5vmoLBXz_n|jh=viP*d|!Ol8wmx!=ymyg z_qjbj-{*XI^Qq6lp!PlW{rm2N*Yod{b+AkyLFnzvi&fBHLc|&NvDI~vQcQ%C=^iBW{ads9T zW1q%v0uQkAH$||YeiJ?(1h)l^iP$!KgDq^Rehpw%XHbuVxY3OchrD32`l3$zf^0z) z8x*0POTaN7;}WlF(J%c5XCSmpix?z{hmtHXTD0*Cde-QeQqcXJlAm+>%met(kx;{i zqvc*h_(g~N)JrE%{pX1!#$kEkTd(_<*V$~s-{r%-zQ^yCti-5+2^mtF-ooz)!MJos z_~XbOSI8HLj|emBhODCMvXy6ZJ@La9u3j~W^FQ65C3{+wVjq@mn4rPPrbWf&XsAz=Q zg{K-7rwM;*BowN6gAG3{Tug{2gw+X;J2QbNFFyQZcyxT>i61{qezA!kX9wUY!+YSv zwAUewa93@F)_NQqU_0VBiY7*I06F-Bj}Z11wQ6wTfN3MFX(SjN8J0_pm~g?OYfObUPmr)KXNYgj2?^ z$9mCdND$XfI~ARvV}C6L&mW+NqetoS#fVS2kKCoc$g4;*2jUj@lRzUcH{6dSmT*5} zL8J`u`?&>sX}BNVZdor(+^>Uv`^qcnk>3^f^F@??io!?LSFaHF%Sqp3hgb#YwG%Ey=jUI3kKa@1_Y{r*S@wGvpZts8v+v&_Ee5uElzm#*Spg*1K z&bz`*qhq0b=Zcp!yFQ?Qo>taCuX|2K=LF;W4(sJ0KhQ#dp?Z7a44?3y@T0Oj}%?{m=w) zPKGdru7V2@TbaDZ>`zhq&JT^IrP9>o26N{%W?27vv6S#hP{G%1I?xtu6OGBWz*nJw zeTOy9Z%MH^;!zN)T+*$0pPg~|O;;QA$Lt9N%MTycN4`OCD2o1=BaU9(hY#x_4(Gqv zY*y>PqUW2{{QUD~$XmQ+uo=XAcH{HU8%Yo@jXDJV45Lm$&pdn77z><`&`fsdzK!PL(8c|5{{v{6t010r>(;2(&983D; z)fVvV)mpk&knRN&>#TcWec;>F?zQ?VY(M{m#niAnX~ckk3yDk_a?(!n|S8kPT(}y;;$E9 zk^WN&Nj?b`ti3Kf5|sHO){Y`ly37)lFgK9!Mxi1sqNp3L8Wo_|Rl`?2mdACOd9T z2Af!xJB=VbCg}et}qzwZg2JNM%=Y}zn3^wl0787Uxo<*P4-&h z`8WYKgikcj2c+=FK&O;{dKY?{y%F^x+){;2JU+yXytY4e+<}Dz_3s)q#?a%iASz=- zQ5uvjkx`!NTB=M$WtO3T7>%OB+%OFJ`A3uZW(U40omu6JNC7z4_XvRBxytu+r~!?f z&~ty03hdBOwHxZ0;j|d)dA`{=%2mFkM;~dufnrbf+3muApb}K_l%(G7QWJ_~RJ;|3 z-R!owv4WK{kcLx${Wb#Mfe_{*Vbw86n+ZwzCbg95r_G~hAS`m85+QfJfnfnwGV4x zG(73_;x~~^=GIpi&2Knwp^!@gs{DvAQT;?5 z5Ex=cqfEJs{p)p9mtDw!HI!ZuuU`YPKp#>$v)a{0EMla)i%CNRU1Qy2ZLLiO>MIH( z7b$KQEF`xm%COiV(fNT21OOBg>k4j&XhwrpVlWhphVqK^nS+6(D-Ba3M7rj3>p9>TsPxu5qzx_3YNIWRakttTv4{cXEsSy$*CP#M$!Y$ehu85 z(8#alZd!7c3BVLi8u`z`Gu^l}y>zJ#iBbw*))iM)Qn*NehJNl`*MLI>2Kt#)S<^o` zFxgy;MW6#vi(NsTq!5=0a^=Tiq+lZ&>0*6}T=AyCNbxxQ


jp`4yf9@8b~F`?RUjYp2Op4hrH>cxvPgKubUp)S@GF)E?3s#TQ@enH#$Fk>~!<&#U;DjZMV7I zmh^b5H>J~|Em}unsJVZu)iktsXmnRcYACO!Ef$-*(8Jj9aH7A(WWVl`zOCix9=Bb& z)!}kEUVQ@uC_IflP;9SP640%3@G(zFyDK}KN(t z5_e5fI8fP-mV8Q|vZ35p$Ws;kHA8(9vtfTwzL>YA|=$moKbPF0^Pt!oosYg<=hUsS!+o!JXmfCBB}*NP39 z#%-XfIq8nan|QEMaJ?cgtc&C>Ky=Vi0U3&fB4P)i>wv4tQP1|PSB&}0vRCc2zXt7KWc$J3J((4fQK|h^13ioi9kL&{A4IQkF|k2 zSi~hbWC)8W){pyNa+yRd67r#=f)xp8Qh^mTXalFAa@`Tr=qtnFBLfTR2N3m?!gq|PauBf}%N60r8{wdOT!*?U41qHw^`U%EJ=i+Igi83OV2@Sb0gGM#;f_|ZK^s9UsJ8dQ%wKfmZRkhi3T{SIq)pfIz z6RYa0_Ww(LwOkN^d*RSzZ>wvs_6s?*26NA?&+Pp}@^?Os#(3Z3vB>LQ{a8f3^ipKu ztT?tRv!&8qJFcya9pBNBNrKg>7Pqmr`9{y51wEc%*a!JAh8Xr+6hE)uQkJ$zfB(c- z1p!+kCSjIOn)8*|#6C+`p8B@YG(^#zritP4zu{Jg3rlE2IfoBSgu zxt-N0N+EbG0iT{qCs?dHIxs!JMPq50z#4k*j>xX27nc- z0IDIurBTvy(R_Hd*HkI?9)f9vpKpf54>b{q2QxZ`=`Z7+n5;Guezr)!_{q zj5(vuNMF-ulMzMpeG)zr2}h8$mB548+{U?Bei}&Ldn4a=Rpzw@*H3@Pn=lwWUN-7U zp|P2#`iwVjY^+iC{CsbnmgT1@nvH|VyB7OTuMlUOee@=S_4b>9yu|L>yYGUHlmg@Dy zyxALvcec0h9Im2;`EjHma18dsXLw8%gHn2_z6;gZ0!DoP>vIBWvxVH^;sksEyJnZD zddOAtkCMpELSUjVE0k1&K?`ybmYi3Q>PI;=j&q5?VuH>u`huKwy>=057EtwyLO|o!t(&{H@?F%lyb>35%>%`&I*OCRZZh!-;JwJq<19|}Z$JT&&ejryLr1Jb>)*hmafEkXKCk5l z%kMDDg=fz{W4Wm|;}f+f)%E9^x{lX_r|utiL?uc%rL&bOR9G`uV2jX{X%P^}JbTV4 zGy$J-Ur$>Lyic(}DOdu+$?x=wy`(vFf5A#V)y;iyueGUMdu2^zcU8Yh+~~cX+SZPt zYX@y@$81y6Y{&g~`rPD)cKhBI^!h@(sz1{vdHwwC@s<0F{(I~d%rc2|sD6N6-}`uu zyXNN~Es`=}CM86-qU%G;F zJ8(}fJ21b^-(1OsylN=dwtE77pQ@j+S=okQ^{bw=k1YmOUl3KQf{&xRNigK~1+g%Q zw8&np^Q^S3vKfa5Mg!D3@C_MqlcAuI39vKRvJ5C4vey)HXmSOwE;q3}qk{wJ<&~X2^cMj%Ev4PzT=BJtf>RLK;GY=&adr$eXW=P;yj(k=zmp=}?rWgZO8` z<+DP*_3us@OfZPd;m&-jBWw#e-bkO`%G{ps80|(QkhW1n*5v0=rZp!9V=(xdhI-=o zYIC#EWA;UC(M-HM6)KNpkrWf_zM{_&v0Cj>ugwApNxFp-tAi?c(4eUABWzc=;b0O1 zRd<-m7ivs}d{8_NNM)ZB(LgqsL7@u2#fLx$N+ z9@#a9n8&Lc_81aSG4$16=%aY4->bd+A*S^01!T0zkB7W;J!T?9{z_C~lGm<#%lxAO z9QRfveVP@c`7{()gb1j~kr{nNWwYrDs))tiFGxGGKu}_m5!elA<_IqX%JG<)(R|yv zzE=AN95GEr0Pk8+1`l0dsLVBWw&Z=v;d~(8hz&&GiX?x)n?Z+-+7tXGKtBa`c`;2| zu_axPC0t>{pBUAkGpc)+{~fxp%Nf?a(|5O_7%aWV4eOV1xhz0P0S3}Ls+gEC-6 z^zKoeZokf2{Y3;=J*_>+n`=+*jjAuP4|&tb@TgwK9<*58F(!y@UH#2G}9 z2hA^NUd`j#Rs0p3@JfL+^wZ?4gUOt=yN3d z%w80Zv@HC<$(|_=3>2FN2C8FtXoAQ>7}zF%+>98o%eJuIi4Xe+ip2qW4v&L_c*dif z0#}F_=4W^eGcR3>`ogoEikc+q$Ou8IQ4Ba9u-kBZg5&Lk4B)j}^}3CM5Jc(LMj|jT zNkq-&S*|}Vf3*!wBE4)omuP|p5KV`4)1G@H@QeK<0xKb^dLvJx6LlEf8}S6eZi4J^ z8Fnu^Ze9)Do~lqGn?>1$6A#V+za3TJ;@CEYknxnrs)6rNEP}vyhln6?dg;(E^@i7c ziJ(e`bbk>-kMi!D@73A-wSfut0#}Z@x9Q!7a-6R!}fdG_sji!Np+zHZ=p|*0QJq7b9xhF6 z^PK=aLtcR|L#@EJ$~(#QV*Am}MrHsB*1*pZ$R)RvXTJ5aUMt8*=vQ^^YUv5FE|TX< zm}v1vhP0`NePJa(O~iYs`R&ANKh|MIbz&wL0O5>y4>sRTe?Hc(M-^AI`Q|7#{|rs+ zeq^I7X8o=7{0xXE!tQk=ZbDk*3{Kz;$ci(3+Rj*4>7;=eXgTN0(a8$vF&5)h&Qu`4 z_Ep~>Xb-Yeq0knKG2{t5|E~164%8a6{kta^ajy6+#J9Ujhv+wieqz-3(oaE){!I1F zp%6P2Y!6i5&-MiZsV(1iMuMJyw*?|m$KRIz&KdTEjKFFXQIuv;{tKY#7J->I%7F7# z|4VWMG56>O4KP>z1)0(O+N8J^^0RjBFIQaqUvw>}Ec_7XIjs_^_BZky$WNn!m>E_Y zJ7_TU8H7VY>z~%hfhbyVJ`SXv3yqUfx`oD%uq$^F$G9u}bTO`y?4h_U{SCt7`+51jZOjxzNz=2H;Jt{yY<4BTc)RP*-|ZXU)Kx$+dDhA_tPul9)-pGkn?mL(qyJGnFfDEo(l0tot{7^ z&`2T6<=ZeiJ)*9NMPt~zc(4I-M<%hJ6`}99WQDDFzf0P3I3p)*V zy&AoQy^6KWpk~s0Ilg_xo{}?!{D4e>KxrsjuPqiic<5xpL?vyAm#r7o=_7O|eUa*9 z6!BB#e&RT<9;Ww{Bd^}e^)IR;^*-DaRbWw54sr}W1k4$(V`yKn=19@F%20&C$8hs$HD-T?)TYyFgm`0vVQ__R?yyp|OLvx!MCCnY2(qFR8w6FWl` zFB8R6JQv8}FbfNc=ZHnYQD}cOL7HKPmm-dPz)hS!(^0m;G&~9j z2}E=n?3MDx(9co8QcQ6AN%~#*gS;AYUQ3&)FE`vzeZ@Y?Xe8JGrLK@2B5IZrfFi() zlo~mCb0&;pGz86A2|r;2g9q1_O6v~}T(aIZfB8V5*@G}btYa!ZbNbS9X~SjxU0XUk zw{&%H>FC%}T8|#`ht`$Wj9k=iFd#Jw#M*!0^vnYThvrL{;Hpk4F5HQ4VDvAqRRaNC)f?YutA-KGxNlPLY*5 zH#4?reA8fmXQivsTugPOJHkPC(vuVxs}n+9=$?XOaf{V^wmc(rR9>W#q8RgeK3`$k zwv?%q1`2BQ_U`VBc0^+xkyuA8*1)r*@Apd@w)QPw6V;isrXhBgk;G6C(c z>0ov1kT@+M&QGaP$IQYBA};x@GY(*5Y;lv44C!UFPPZ>&f43b*;sfxFF0DqK zvr1^hNJLf;gt>WQ0M3@3Z4{&|KsY9SXOTPnb^4^uiDMeC-d|xfEHvjM3-` zy|<;ZF=NO0v$oa8Pez(_MuIVH+*z~KjE0Sb#uGTaVm<{w1M-!PgvGOPRi!pFm=@=s z+H$*1n<&sWGejpVX6nH9VWuCa^T*oBlR$KjO)(m$*&nT(b@eFTK?%4A9Cr}i(f<%6 z#YMj^j+~BZX{WTOa!~+*2k2pX01@@z{4kjhKm&wF(rnfp0PLZkS1DDX^C(aoj7I4I z!5{#?o1H-siH$QmXLpW{pu>H0Hcc8E=o9s1g|1M8B#LToM2@|P^EWbgr0MFF1PW<$(dU>ZMk(CwQE|!!^iH~v-{qw zhlj7ecX#vtRHC@%b<=&D+uJwy^=)oz+x%kf>-2Z;+cyt)H>Y~yD1(acPv5%bgOOPO z!OEU{jt$p-$Ln+j(xGCIO|@<56+hIwrH#%&>*BBEr<5o(`zr)|&X0AI58%*h?p)&Y zD2@kQ6QPq=`;>z~SNi~814hZor0&j43WXO?Pmf%7lVa^~Cd$)A3NPqL@ zTwiQ_XyBr@cR7MiCv&{3=Y*|&$I!&pBjbhfY+;}`8|@BPhn-=3ll@hCeW`t2sdZy# zp)#DV#0H0x<>9`L_c)CXN8I_|!5La79Jv^D4dHcVLZb%%72O{Yaw0GVoZis*kxuM| z&JTT4^ST}&#YPJS3U%68Gd!q%Gz+Sc-W{E=WncD1&~yc!rY&Kdj4evvMZGX=BR*$H zw{RPmR;L$E6Yqkr4;a(W!U`e!zpQdejG2vN3Ax>RLPti@#%VRt!UL=nt!MzvGw5`NUDQ}gcK`rs zgKqOce=f_UiLw6qf%*22=_KFFc!DUNWOO=-KTw$_xC~Hqxor-edcbam}KK2|w?QD@hVTO{l%Kp)=EVbC?^d_>!o9Sy$*(gGVpfm@LRwy zB6Mlu^oOV3M44a1r3wi49GC4809=C1J4-^In zh2x9*qX60);5|?gX|65g+1Q(I>hgnyMJ+B;#WNZ2`Id6pgQD_Rw9l7X=G!KH-w#@@ zo;hfD{dhLt(UG5>InO$Nz~^M|bbE6XEv2=EyPQ7v12(CxHaTV!;7-n~51<@G1r)JPdH@a}UD-i(WS4V`+0sDU$|| z<#3HcKHZ#Yj)a3KQjVOn6ieA4%g2Oe1wozxk=5NEz1>FO(+bqZ+M!(O|?8@tg`FrSQhUvhZIJZ~a&<}c`heFgsmqN#Jd@`^)ox~jhb zKF$8!Z@FdnKgj<^&sl5r8+Q0d-8Q?+h2#!GHR+Lm@DTD3T%e#?X_s_yWjCaFVA3j=PzGl1iVkRzd1G~bJ0 zVN^K;Q`OEQ%HmTZOQCMNkrRyz9EWl~GwvVUXm{}=tIr)UR)Yqo8^y;_mfX`?O31R; z;kIR3GX1I4K-!-Lh}G$ZORKq+{RXIvKJcf#ePBuH0s1`EpH7!80k_pAf5VBE8RmsH zg!A8otNQo!nLQD=D<~_LSgJph?oXkjhE5JTU15*8`UcME_|NSrug8z2%jpzCBmHSE zCs1kaQ;LE6cL(?xZNo`~*`OSOn-N#+BH)BXr6A#4T&=3H=4fu@@Ml#v{)sSKVZxSo zm1FI^$G8Lc2e1uP8Xy+&W>j$K<)s1Y_kvBrkH@mRf6E?ZtVyBUGiAW~$kJd2Y&00f zq~oK}nRPdzcwOVzm~;=Sa{LNo<5h0Bnu=MIGTNArOypWK8dbxyB>iEI38y5UlT(uR zcNV|($ME-2%#YXi#72Tg@Lp5AzSp3r@3mTX_0rGNsb6|^N@&?NZ9Bpc=VRPGT7}NO zaFTQrx}^FEVMpZ`eXPpSrdVlc*i7vZ1SPoiG=b{gB~NbY}*o-Mo{g)E43NbCq`y+poG$QTMSLJyJ%0geTMeBVXB zS*q2`1%{18J+d zYC}O%gY`E$gX#i;#cB(=)yTk9kL>Q9!a4LL9o0*{iq)KSv%3tc-T@Z4@H3ms7x6`T zJ+9j7bxPxvQTVC=O@~^k+fSvT1ay^$Mp++xoZ3nfsF4I?7{#HSc3xcvNsuCgsv*FC z?xoe(JK0jTa z09zti!rp^ma)OpLr$(X9;JF5R7T0X9>=D+vr z?&;a?()?xpbWQX8L1Yc2=gpw+Qdi=2fQ)E7GuEejmpY4b3jgqyjIs=dt4!}-e&k))kaaKc=e0YHx~a} z?^N2bD#2$Ytu9KZh}14V3%RvU3Blz0+F#N2T9p+9U7 zYdzS{i26O+NcnPdKXZ38_HA0eh210ng4cNL=CYK!#9&2sku(8cp>i{DCs59NV}VD4 zS1g5V#o6l?l*(ZXcLXz;ApZYiD3uBY(`oq@R1nW(0-<#6FRl7M`F81_r^wlT7>-)A+i*Geeih}TY!oB+0RP#;@6nX3$#~k7WyOCG zFvzOeB?-T@IA=?F?rW$i8UMCS%-cRnFN?aM6a9+^tn$&bUroqXAUuKW?)bZH8<8Vb$y~Zrf&;ukyP? zu6dMLoOgxXe!YHZ$U^&?l272i0c1kO_oAFXK$7eHNS5VnY6$ zDd%jbojNGLl|3W>>+j78xrAIGJxhKoHVhF&AJ1^^k}pCn!z*z1)^G~wPN%3N302jG z|GXAz23c^8iyeS7ieVvmFx)0(S+=$i#CU~XFLW$O2(Axmiax9|G$j5%o?tHQ3530l zpb7QF96qPFJ=QzH##&t=`Jx~`HS6`NTSdQ4ox$x^Z7%m{dnfw6#iQ7dC^$toIE4$A zfJUzlehQbd39=y3M?2%m2*y<7*<*DFe*?mX+l?SwoKMmenIm8u(7{a?)D`96gLt8! zo}x3PG@;YHd^C8I$?S=_?59meko8S43HU3WN%5%Iqb~UnyRPvxofDfv${zHf<);Y; zh)xiVgRdM!JXNyYuZBt}6EM>P>m!m+MOz~Bp@=VCP9@7(FcME7o#GaQvA%76OsfFJ!MYlKul}LsQ0Dlg^&nC&6@zgE3aw(lT$Qj3G z>P-(?;p_qvG-TR6I9NVkY9Pm+-=N;tcm>EU(6=AXk*F67vO6f;VXu&yqa$p{vY1Y zh8yzSoeSHBgM>u!cTfvDOw?!@V-UO-jqEkR62`P=9@Xo2I#UBJ9c7G%s%#?Khx0VV$I}tRLC4_-+UCqspr1>EOSvT2!lZDJ z1C}(@E)h|EMR173aLC#c2cN37&>p{ca4zhzx(!BX=MmJL4p@PWM@v78v!^Xvd9gF; z38O!+OGO0+y*?UGdAxCd+SZl}cf>Ky>|%h0fDFh=y?mU_#D>oB;2su9gH_c>NU5(V z%hSM|#Cl;wTrZ){==D5kO;drud=QSCZks~~Kyid6lSps1;bbT6P9LfrCVrgiPYpJk z-KJ1j^|u7{c7x3&%R$r^_B)Y(4oSTzu}jel?EkeaMTM{a^jWf$PRb5;_u?bJgM`rb zO^{9I0O`tHLOc>sNkFIp%xaGL*^eOwe`gI`v)oRR^38YwSOkVZeCTD22$R&o;>u-U zO26;#?&C`RKlsC8KZ^qij;s=uD)4p6bI@1q7p4c}%n0?2D@=7o)eb#%uBNA|HmIqj zhg}Ly$@;&&yREaOpl_L`bm{85OZPtjRpkF#54^Nd4-B}49$4c8=^7tEP2ltKflz?+ zfd*aZTzmiowu}wfT#anNR&3+~5nCe{$SpAez6YCO4gM1HP1C_u$eWriqUqphoo1vi z0)*Z@hYsGTf?JKGg1hb8w%O#(%QSEcDd1!&9)YF!?cZBkX!a-WN7holl zi6|dQ2gy>zOYJX7^rU&;$JWj}uF7$-FhNU=% zV?t03VJRYx4?pdw=BL%{xVo=)nS!>Or?$RTOGXVN!7rd9z-mFgp)nS zg_F%?Uy@Dn6|%|VfM`+zNTlMT2~_<5+|Ghw_5Tw)D`9K2vl1Q4?5woEm1X*((BNE3Yi69V40>6 zDmAG-bs(P{?@Q82&dWEV;{Z%YX?Fh6R^TtZRKEdM`IUo5vj z&ux2xmY|D4$zlF*liTjLTAXH6-08BJ)y_mJkG?rZ*q3pq$7b=CMfR=RAjm9Y}Qk1kiShIY5&tskVorl@Oh#W7N1GfF5*@AsP57dgCxi2xdsXE zr=s+mecb&yXz|OZLSBQ-bc5H){hv1p|EG2@L@4)ymK5?kBJbjVcO|t!f*4>@X<9cl zZZOMpuVhRO%W_H&g)EGv)ceI!rqirP^Hj>WU@5I;v6NODE#*o_CM+p@bumVLG6Md7{H1aDPVB4KO#FaWzMpmz+sdodhC_hR&I+uphL|NAc7K z4UJ~>jk7#UXvoswUA+8k<>~V^MNT*zYoq=#M%^U!R65T)-lj;?g^zc+ygYljZH>Ox zRYv>FGNB62&DU6`Vzg@rVLm)0Gb(wAm;y>4qB<@DV(_dJHUnrON-_be1&!(&mRwZM z2(O8(S(*mrZ@u=ipVyUh_S{-D=Z(BD{ zw1U1~uRMmi4@+w*Lx>9MQ5Xpb0-Cmw8qb_HSt^Wzq|M>T@C8lQA(zkTzpL}m7h0gC zt8O@6(%hZNT4qA$;;Qi2ajxoVS#8FDz6zstIy6R0dwmuAit-KkLW^{sGN}7OUSqNTqj|?!xXa{9FUnFgC;=+SQA*-Y2@o;*%j%u zY=m>+ji5W~Jq+9vaWPu+p01YWd^R{78m`Tvo)&zLb$^ZH1Xc})#A;LfvOAOydHo@@ zbcVOo=oH@4wUt(!r&VV9hLAg$HyLz3mruocg8$TKbGp~GceWdpSPV0727R4Y9>>fF zrAlQ)>=ezB2ntn{P#P4NLYln+;KKTBJKIZ5=yf(28oa>Sp2HztKTxPy?LTg{8?AwUIWvUCEsH7`}O3u=5 z>3QkN8a|`u2Bk9fjrfd{I2TLt=eJP282>+y*ztGhc{M4p5z^u&=|O1+tOsCRh(HJy zFZ#X&MO!1_MA36iXA}sf-VFSlJZ1ElFEWb%BacRWr*}krAFg$D*(BYCOqbvN_byXI zeHZ(~0s6g75#K}N_lDsee+c+rV9+XYLQ2%+K!8TK#18cs-?@4JOI@4WLvC-NuUPD( zzwMjkqv_%QkG=DNucBD{|IF-3C-ff5NkUHuY4n;(RgfNvCL{rJ2ni&iSpYj1JBk&% za_t_lTs?M)DC#|S#e!n&dh}Xj_x!&zyC(?|(QCQyd;g#JoX>A}c6WCA^Lu9J*`4Xl zH`g7yCa_8xU+wfBm7x1krOaD*IWxnzYQByLulMnuuAVesm#o(8PB|oQ)KdwD#k6V_ zlh-QdTfJwa!zvUT2Wx=Ls!F}0KC<$x9oECvbJiQyrw~P!>r&S(t~;5?sZCfySVmZG*sQRku&S^V z!_Eu4Htf!@N5h^Adpqo_@bK_X;r+u$gdZJYMYLxgyde?U5i=q#jJP49foz7*kN9WtsP@K_UJgMb$)3+Ri6+KCAP^ov-b@qw{^8_jG=t^P8PN?)*&`-UsUv z*QIZlj4orkOzbkh%aSg~b~&}{VOZ48x?n6w@%#>yIs)j+wNiA zTX*l)yOQM`VfXUxYr3D<{jBbnbpLzzo!#&2?(hC`kC8pL^-S(LtY>b| zX+0P8T-vj`=gB?K?|Eg<-94Y_`FhWfde*xqxo5cx-OJpo-6y)wa&L2c-S@b6yT6SO zkDn2LWc-8iUnJxwOix&tP?m6C;t`3y#NCO{CcfTlcCVsd*Y~=k*F(Mj+3V$A@AUet z*N?p;S*NCF?~L9Pde7`#*t@*<`rey+pWpkc-naC=yZ0l#pYHu???9jBeKz#j+~=~s zL;If9_v?OB``z5{?tZoXp6d5XzjykN??0pek^Rg1*Yw}e|KkDf0VgC`N$rz*CJjhB zFX@V;o09HIs!e)2>D8q7lD(( zX<`?2Be!GpFmEK66s$44G3pXZf5}bFP_l)12Gq z+&iasZtC1&bH~j+dhQ8xH_!cG?q>!43Q`N!7o1XX=Dgf_Q|8@1KYo7t{M`Ao=P#Xq z^!(H3UpD{N`42AWv*7ZD2@8*3_`{LMANkHvGmpCdsC`GhQ#i2jqC$Vs^rCr1Hx}Kt zsP&?ii*8@^RdHdlx1@8)mXb?Lt}D5{skEl_-qNoZPh0F? z(rw9cOI}>sZs`+CUoDF-JH6~XPpW63=k{fNmQ^mhVA;*fb}f5(*$2zMEsrYiQa-A@ zvV24NS>>-R@3MTr@>`bQzx?fr))mKBoKcxrnO?b|a&zUD%Ewm771LK-wc?ePZC4Io znYFTPk(lB$MOO|81P>V~Sjt3IoqQhjsvTQzYt3u{iQ@zwmR=7&`- z`@h;+UAZP^%?)ekul-=%)^+b4oqNo(W2YUv>$tMxUR%=2ZTzq2fNl_=Iopj7e&z#);b6a_cD{ zomzP6HJj9?=uMM0owVt>zjgWBwVOL{p13)%`J2;5oVMY#U8lD=z3lYI&KQ5jM`vz0 z^Ub8kAY<$2d`4cj_+Yu?tww$9pm{MHAz zet3Sz^T(fm@%b-a5OzV{1*cx{%7x(Nk1pwW zN%kcbmt1_wE8C*BE!cMcwimVqFHO4i*h{aw^xMluUUueXzRNpbzTxsaF2C=JURP|m z;>9axUisuzeXrVZ)el#%;(y82yRQD|n#5}+Ub6!2hFw-zU9ULfsSm_atwwz%!mRf> zBVO!xD=Qfbv3Fo)`(W}{Sg(rK8tQRTVnv9t2Sp6m zVuC^JEoQIo=KY616DvjUgT|}Vb0l#dDLU!B>`T0-+I!hI_yzcqbCK)=>}S~s{=`1h zx&99BF1X+4d+C7&7Z8TVhGv+=@r5FWC#Mo?)PYcS&hbU0=Rbin#9&LqiOY407^{|% zKA_L3YTO6I-+5#2V9<4YIQN|^S~Z82%e6ll>J`zF6-XnRL+xR-?*bm}ehvFYhH8py z#2DUd_{-rMEyif@?dLo-hyB6uAm=X^V_5k@`x`@@{MqOyD@Ds+%Va$=42is?K0%C< z=kO6!GmzJ*e+l|9XZmX*_GcKtm_QG%{Th0T;i@V2iI%Piz;{ndx1E#u{^r8vRSY)Y%$U}@sGH~J6?X(F; zF#3_eTBAt^w5u%qX-pP9dHZsn@;divS#!9^$X>tTTS_BHn7XY@N2lE{rgCknwTrfz zNS|Q|_gp91TYbemYY^e>;aInOo6ANb`*1?=V=ed0B`2)Yxt>kB_lfqDeS5-AG-*y3 z(ei7K4kZIhaNVRq$E)L92gZqXc@pI^i8`GwhN{y< z8&!il7Yv|%OeEjAVxYVozB`R2zLw#h$jfZK=_l2Q{_<#!FA(u^cyPP5P;_DS^)A+I zWHNz0_gSSNk~ikwV;eUuc6azpPIZ|W+f&bn%f=^jD1pVrP;8W)K1xqaeZT+0QD`-kI_(OXF|xI-e182%;SVO zCUh>RV0xZ-P>Ao969*Pvw~&|X>nxGM2ON~79#hp%k$T^rEh%E}iqd8OuGlET#bR8s zVvrav*2@Wuw4NomFxKjoe#TnglOL%#HBe=!p{hXLs~%N*)IQ6_dS-*R`%2T{~R& zxE^*r>3Z4qhU){@w_)9ToZREG9^blKyF0jhxD(uc+^Oyi_XzhW_ZWAsd%C;EeWm*v z_ut*uyKiytbl>6jx$lb)i*Fa-F+L%_Pkahve_P|POHc{ntRum+^a))PdM5NwNJ^NH zP?S*G_k-^h-!JK(p8G!)3(FO9lF8>4@>Y}2cjbrjJ2gP1nta})c9YM&3`mEQ&o<;U z(duLJIhcIbkk6CI=b6?y*45Sxj5|M0K7}jJ)yp-~mFrsM^0=y8XS=R&-Jr*yT@SjR za6RRE%~j|6B&=(XjpXwR^4ZSa$?Z1zOm`1IAfH#ex0BBs-8N?hmH{W?P_~t|*-mHGJ)tlk_e-dJUo%XVS z|Nb}j->Y4(d>np;BSrekPx2J?qCRhpvc`n=);Q|~i>wN1nef;yEbspEv zHNCa2171Mbt`@Fr>NDRp#kJVAf;wI6I>B{{Ym;l6>nfMmwcEvaaIWWdh+!v$H7VW3 zIKnoXtLwu~3OgliQy3{}`m?VZA19bFPO`6tZ4BdB|J2LsX7!x9gBdm3S>4t1%BSv8 zx2bQ{_3A-&o%)BmLEXp<5I?G*x)tjZpMEfh7dk+U#D1EM4QM7dvAJ;VQSi}8*u+l7 z+Okz#EN&HEafi55JfQAZcc}*~R-@+`U>CNR{jkD^A}IA#-BGfO>?V84-m;HOmXqaS z7^`N=1+qx4k!$5~a=koN-D~w_g}X3pFt@0E>QnWG`Vvm)X&qs?<=N^fD_lKf#apvr z3QJwejE+;R)>aR-!MC`mx|;rDTUaUzHg7FD(FWbT+tp7b!L*0Nz!Sw} zIKD(w!&OVg@uE_Fk4@@Iaf!GTtKUDw$#R}}LDY&T#cSd@@ru|d-V)!4kHjb9E7?}Y zNG44X@5v-Ng1Ki0!Er;>4>DhtVOc7bOJKYy@_5?(UfA&w-d}wgef1`K=xyEydk2ZA zL(jaAuKGX>6W{Y*NHc1nmAL=66eUd;%qrboF!+A3*}MFqJ5+|UoI54$kpOT zSuM87VsW!vC9dbG{U&;JJL%EgB{zz@#e;H_cvx;056QpD`C^qEC9bCTH%2P7Kpq_M zDqk7DE;h>X*!`YGW4we${E|6azT>U)mSUQ0FDkIr9xHRiX>z(aUCt1f$;ILZY>r!) z>h&U7C|1a!;skn=E9Ee8pF9~Yk!nq|4z~`ordxBY+16ZZCMysPv4$bJqpcBECTksx zu|`@$t>M-J>qvE!dPx;x-&>?gRH-^iEmlj=jHjv7)d}iUb)q^&ZBi$zzp172VtI*t zSe`F^@@{!AvkTuZ{~_;^56G+J74jx|8~W^Ad7j)VFO(O_ZSqoinYa z*cI=Pcgox4J@P^MkPOI=Mx&G1LQL*Nj|HR<#TGFd|uI8R)ge=%=(=s z_o{T2DqmI^@)eaSU*-M8*VGVJ2p%e5SHt8RYPj65M#wjL>$gsgl5eTe@@+LnzN5y< zchxxgp30K%t8Dp!%8?(cTy%V%xLqDAcF1+&HhB#1u|LlncrS`*@lVkLD}AhZnpM1> z;Z0{hU!m`&&+wRt5PMj`|(7 z!(TCA00#X9=MN6Rd+Mvg&CFBMlwkGMvb zi|w*P{GG9|>*NY?tz0hd;rYNPPhc+glf?b#fJfDn>YwT{P=Or-cz5ekJUTsUGm7R!6Io)y3*+wN&4!uh8~!Y z1SFa4_KI0^^9tPV$#;pCM@;sH&zLvYn;GZrS5Uavy?NGLuj*ZNH|vLp;^M?baq;n9 zQQ#FhiMcyr}a+UI2N z=xybARnE+{Zf}c3963cLUe~m>J27<=j5oftTf9EIvt@@MRpO=o1Jdc|bzIi6!?J?o)eJti03)mjv5 zA9sa`MFj;VMFn1&R8ZjLpuk;1eiE|_271Gi-Fa@Wt9KE3ipZHZ*Bgg>kZ$=6%2t1z$F8#W-Gi#hJi?#;xtBQ>lyC3M8x!fA0u z(+lP%7Q`30z2j!gLJvx~fH(PFPGCL!L4 z0Rz2J$!?w6mSoE%7U6~Tjw>wCUJ7$fYNL~PMzs_Uf$T!#I*yx?UE-SF?aF|`(#`^=i8a{ z?UQ$iwmGxr?r7UK$197ny={`T$kDX2ceK)*)@;18BXwc*o;G)fmP3jwdowjmw5oIS9zexV9?2+=N)LeBAuF*~J{rag2>7CVH>n>p9p zHZj|s=WR*zi%q1-(G=TrrjRuJqciWTwSj80v$J(cv?n24+tEHU$$MH-TmtR9Bbn`x zG|<~Ad56^d&J?`fcS+u1>3!Ga9WK3(OWqNt_ucr~Q}4Sc?}*U*9?3f*^}c8FjwroP zN~T!sitvUPQsjy56t7&U#b%&4xyi+j4HsA17Y8=E*r(xQm3`5jEWE9feksE`uXosa z)fsJ?zj*TJCS3{SPwx}SpWgQ(e|q1W{ONrk@~8KG$)Dc$BY%3|pZw|l0P?5zDar0p zMxq8KQ^cJM-LRi5)QzKvy*EWS-qd98pd{}g1Tzhp7*EqZsAMJ<4NKGve@Gv+%YokX zP+jk68J?$`o;Pj4jxgCFZ!YYpYcZoq@%)mX%w+dqlk&lYW`~pabH0Fv4w_nhUUa-& z=wI%b#9=!!We1(tA(RIhIOsivNKMhOf!?9XDV;_Q^bY$que9)Dybq^}M91Fl6!&<| z4HWIf&6~$3j)#lpq5+{_RMRlY@=KKP2uQ4>w-er6@M>@4ZAS}{?TyJvD&3rt=ys3V zOqe4N^yW^n!}EqC4UTWOw@{PIxFhD?=5mL*<8E{H3F}&rttmGe_9lK(keFZS4X3sb zlv_kgCfVxAl~Y)f=nX@=!g{WpqBz`z1rSB^dx}UJiXbt+Xjoh#G31j^b|x0WJ4hrX zM00aELQG|Z(cn-e%~C>$MOtm?L|AMcN^Ojb#xg2Y7H(1y*2gJ}#8DK}=!Q$)XgI{} z&QBb#Bh|GyCUnhY#x7~Em^nAaJqpdPQ?Klk1lmv)-tgWWP2^HTC&sSCpSQfiM5ne; z0Rz2bL$R6CcdDyU_jH>Zi3ndYb4*k&aCm(oUsrg&3HVxGyeZpMA= zk}smCmThNUH|InoEd|N5GnAjaBO0ZnTfb@f|6Y?Gf3ODjds?wBZ_Q()5{JdbH)${N z1x`B0L%<`FLZvc+qmfDRy3TaDIJs#kf{7GC2V3mvrXb<%QoKWu@=3pNelo$y_U*hw z@iQgaJDlBNx)k#$Zg)P~I8<7PC+i07J&aqqoNbNWxd8agIrsFVYW@ta- zxinMz(T-W#k9N$~ezapw@@;To4z9VlQ1ZA6l5e9|uU+$S+2`hKe^Q@Yp#2%wLhaAE zj@15)>nI&(9^MLdoZ3;OX!Kb}TXJ%Eh(Rq)WTX zOuDqoW74Hv%S^hotK6hZyOx`DX;+0wmv&WB#-kc4aD_SYj^n~g+nI&4N*B8J=FR4) z8eNN1`>C;=+RrNEM_R&(>{hEUg-BXh@E+o|tZXFIj`qX}oU9po{#Q+qhpc4`mD z;de|!ccY^KI9!|8K+QUis9orDz$>zv}cZ%)Q-cPli z+WRK%9BYU7H`}Q_Y_^@+!)eJoV@wy<8y>fl>A~`NdP0{KWG8teOTAXFX=_6YT^j%d z4}*_Qr)w$CMBUkse=Bx~i0nJWn3NtaF+jV=Md!FOA`>G!M7W}z6XVs$@TBl=Vdg|k z_I+)5w2l&t1G+>EURq_}C&oE{edcby@7tEUqnF%t1P=_G<~qmy^zmJhB6Ol4Zbx5z z$``p&NY}Vc#WNkBJaI8Ibab8?9Ns&;Q<$sefV*VyWUuQC6nO4!VI@otk*jm_TYmGf zaLf=htk~cWs&DXzAkWRLer;S_X}D&^szh_O6|;`EYJQz@RjxBC-H~wWaohKIX1s~N z6(>*7Fv)4ds1w!-R}@EI0^7~^yd2qX#d{5%9k(ThVPm$>yPEh36SEl+v(JZP>$%-= zM`Hid<9#~(s3z_hF+$cm=UX&!$4a+Kbkms95LQZb+tuwcT;E zy0W!X5Z;?O=;iA9XX6q_&y5mq^2tOaGfeP4t>`%2T; z`ZIpx7HgdQ-C{bvt3efalrjSA7Wsr$VbXCpVJzpYn{m>gr#pd|e}2|Y8!F|lGDfE> zNl%H1VF@WMC$uU?R#PaCR7R9Wa(4k&^Ng!`sSIp(-)xh{YQoj2a~}|no7C1&rbVXQ zDk+yTCkOq=RT?o4GqI)+YX;+0i-^CN@!kFhN&mo~rGFAJE+uRoYZm)uCMD&>RYC4_ zJ?omW>qXb6r@?;;*Gox3nYmlnwyyI~ed!iaM%gzl@fqB^7(Z)_UtRLHk8<|KgjHec zO4pJdyRJ8#d%JDTU>to4$5Tm<_S-b%DF=k0+d%UY)#apNx7CBi8%j-)Xn2CYnfO=tDVaIQ-0SyKW~rCTjLx` zCM2KJiE+898w@cvMFzJavj_QM#TBs59=`WvrXU%H?Ic1xDp~1TMq__yA!8ZUZ^=k} zE7@AMk!=~Th(*RDm&tOuTvo_Rxk9d#RebwXgZ*MST)f&? zoz@xK)<@Ve?vcmHV~rJR0~VN#;$5sbxr{BJAWxJh$&=+N@>Fab%dl^3mZ!pZ8tHix06C&~Fnfuozs-xb-Csc7rQK zHMWDRuxeZ_uaVcv?eg#PIwWh7_*PyoZ;&_2n|Q|s+X8L$B}Th%Wn|mSXf{>`tSq-- zWze<;>=TTL-z|N7?RJm6ml1MpwRk{2h~;V;cB6-|XY7)-u=R9C#2*pI$ldZ$`Iy`z zAD2&Hb$OC?4E>CgKP8`*&&X%xbJ*%%(6#}&SH3K+l&@epdX)jF8Q8#PVsm;;?vt;} zH{^cw;hR{sevozYEv!gyV~cu6zKfmzee8tmnN8?JY(5`}<7Gf>aO@zTVzJP+5G)-b zdkD-tPktxA$I9`em@gKHfc%Nk_#iWiF&VM4#6sl~@9{ZoxVD>!2gO5J>Mp~25^d}! zu~<*E4Mkf@+F?2A;8;$&Xp4&Krn;*hs;6?Rc$J_MRWH?B^-+CQKh+-#eUeJ%eS{P> z2wQy`R*(#psRr{U*ibc04Ob)7NHt20R%6syHBM!zY?Y&ORi4UM$I(4)3u1>~Qbt=}Xzd81+ zGt`;tEOoXzM{QB(s`J!VR`9w&U8pWn7pqIuHf<+Wm#ZtVj9!Ip^cr=o+OGbNz4v-; z?^QQp6TU^=s=R6k7LME0?bwI!#1?#)y4%=??^X9<)qMa9@k8oiwM*5iM|hv%QEVf7 zjBP~QN&M<5Y_HF#XR$6nuU=3uVr$!rwfPnGDi)G`>UHe6`;Fz~Eo>;-Zt@Pz*Ns#jmDZ?H^%r@mJ|V3Gbw1$lo$Vn?wU;B1e5wIi0@ z&RBT6Vp;6QJ0xMeT@iu3C5lxHV#K}l>%J28yjc=!F}FR#qOGuH_YgPIH@=!4##LCS zPvs51)~pNB25a$Ht1b4Z7TC56#Tfd`t*v%qr@*jcVOg;{Vhw&o+-_{#|FAlV>%|S? ze%{Urqc7Ik*n3zx&+2A%w|a<^czgRk-cNU1@nXC9+Dfnzu~zrC`dEFjWcRlQSV>m0 zIN2J=8z6(k`B*X2taK{_i`c)gZQX_bI!nZ3C*CT~6X%M*qcN^QziqKH#Wro<#M*ul z^REvT7h1zG6Ya*jIRY!Ewt0@R#)?GxpPR7EpC&eo)2(shL@Ue6#$NxvIKj#h5A(iB zo|SKnw-L#g$8)gozk-c_pLp3Su}ZDQ))H%}Rc3jtWmdVh+^VoDtrgZvtIDe8 zJ(pG1YHN+PHau%lRq5)|h*cG3X?aCQgW-e-mOInJ@;KWmtCoTc}v z>8Z~BS)siNhx6$XS<8!xt12rZvMQHUR+KJ_&Z;V_SW;BHs-`p|E6?m>a*E5UidQXP zTwc01I;W(vrl`2Mw4x?Fx44KK*;Q2*)r9An^*D0c0Cpo!JQRO-nookAcWSI(2O;2~q zCnHp}PD9Aa3!A#MvZ}%^rCcXdd7;o8U%5^N<)y|>UAl^DuUfUdyl7R8$xnJ}Uc^)< z>$(xqDr2TrmlstpwOfa2Sm`<8)9s8+Z<4XWPAbz(s#4R_(!*yMUokV96ajf*azr*0ikfSq)`Hs7{&Wh4w?jW;NzH&j~o+DVe-b88~^*%Zr`$ z^E{^wiJ0Z&d6r$>VY9SAgwHaS9zMs;#+=4%M9nEFE3GQ6E~^fov!tqMb!kjN<3+pp z^E1N>%q8d0DV+?bHu8ss&ojYB&ueI};YAG@rQGxF8?%Qx`!r{t;p_+NeQH{cQ=M7n znEP_=>siiyS;L*};f`4vsEu2+vM&wv+PgP|Al!lsYLaJ%A2O zj#JYI4-a2rVvbqTm`aoBwDgSVB@LmwmTFp9+UO^|tfAZ~hv89WDdm;L#V+HFp)sDu;O!)23~jM&NmXfSMR`$0Nm+4txhc@_ z@`kd4HF8|#q%yq1zEsiVQsxk=lm=PZkWg5|Dal-?m~x#G&2<_=t|@Vn>l9DAQ;Zp* z8gLp)PJUQrqXgwTnavA@=J?8W>M<`Zw({o^l$sw=>6C}f8rdSm`;i0 zh4w?kRyF20&ndrrr(E(vCE(;dFF$tG&vTqcxt znP!t|W}014nFzbCr_A9tS7+L+nwn{sRBEQ(bzpDTN+t|x{Oern`)mSdZ;M2x)5bE> zY|WJEXsXOKyUk^$*_tae&2EdCX*Tg^rkPfinwjpTBi)HF-AQkHD7}vVbi?}iw^<`I z-APZnlfHCYfoGkf1suQ2xaiDyh{H2G| z=fp=B%aoI?9C;SY7r4|T#H?i>#f9Xs(1ck(gZxo^0We!DA|nwjO?XLkiT&Wf5< zT~k$5R#UvLmE+>La#?BdIy)tHr!qA&+X>m}V`bXiv(!wRm)Scxvil^dnRZ`p9*)7{?Nf}PK8BWa(c3Rrt&~+!i45wx@obt(VTE<|f1!Ooi zG1$p(hEq-%q2rK0Cx01EOUQKM%XH$)bkdvYq;s(2KhsHPrjyQ0Cq0=?`ZAq#Wrorf z3g3x8(@9^Zldi!bf1z}T@*j%FNw3pu%XD}+bFdT7U?-f+Q2Lzs20Q6@C^vJ6<8P>w zpP^1X!$b8DI(G6i+)3AP=f2@iI_&NUE!nxx?#^(W9kr^VWjn5BE!zp%?k>>6ose^! z6x$CTwCqqzb!x)am$YCfg^s?=wDl?Hos`*{kCyAC%+?|tJMr53gJUN?TaVDP9sl`G z`1y|ie7gh&JKQ)pEw_bfu^AaD)zztKVOixXmKM3POKXb4#uqJLUS!&H1`Lt6qPh$P z=$gkRE04=Fm*R3w82a?y$vMLdZtUsO`04qK%TU!~@j(UULJk9lfZnKgZBW!Q|e zCCiJfnMJE2X4yem)0UQ5Ibd3KnH^2m@Msf%O=U%8bqgm(a}Y_4#u23>w68ni+XqgV z<{*0N^3o-aC+$6?`C2)iLdSNb^gxPCrWcW)uq8UvVI`&IHANAnPL_}6f~7Ckm@Jp+ zEH5)zE;m`W!^zH36|28V3A zqj=&sK`d=ivy=|0?XHf|VL9#=CN$?Ds&s8JrChSe^euQ!$m_VGw5qJKM2`pX5L9A% z%Q;yy?TGE?4<0j2vuB?gyQ-q3w5qze62FVeV^^-K)VHnXRB4I6Cw-``9M}(!DyraW zd1;x`?1$R)lbV&Pa;6*4so8^Liz|_X<>aHLsA`?7gifI=Z&j74YkKD<$LtN$@hqR~ zDle-t;c?Q?A$vmxTpSXm;%V=X$TX?8~?J>BV0keTSps+y&`2U%1eQB+k`xn|Xh zXtS@<)f{DP zQKnm}T{`a645wd|;q-$toc>^jlkJSutSHk>ZgNLv?9$50WkriBSCiU?aBNP^aQd+s zPCqsyExQFh;6&S6YI_@R~tp*6zY~%W6F01hyyf*Rud_VY`)Y znxuGNvZ4@nH0B<}j+V%_H(%OFz6+YjcCwtz_AtI(k$mB_neA!v49po@Bq8wi$oXt9 z!tNun`fO)=9d;hc7bLf`y;I)F_HKDU+efh0NNhEavVB}W&h|+xH4-~bfbGBJr))px zyIzU4BwR?oDv4s-TD4}|PIYG6Rdr?ClQ|uvwt%qhrFyZ&0>Tyx2-|^*)v~c=Ok;}; zgY7Iei|rgWhb?vtwhI(xf+b@K+cL$Rk=QMkv#nH>Y*(t4Y}cqYY}cuEY>!jNu{~a$ z&h|A{Gm=;U_OpFQy~FlB^&Z=g82Ois`G3y#OZ6oaaWWYm+ZaahCExE%W>kH?HJ|NK z)?)qs0}y=EB^V{w3wN`6q?O~0X)~ft4O-ll#p@mVu2$wQN`kPm*$Tcu>4*JF<>pVB zDO#2n)l^XXF3#)q=yb@sJy^?1tC*``c~R9e(RKN<<;$=Z=qviCj1T?xh{b(je7Vwc z!IK+K+2}c5oKVZvNDw`+1+{Uf3HQaM3mgB*uO+{hbY|l*u6q7A@ZaC~*S>!DKRh{{ zI5sx^Inr_b;y<}plmC&-BzM3+iRrpRf5!G3{>i-75&E0--$07PlP)xI>uo*9z08#h zb%;q^*D(KHNma>0hZ-}@DtA2_Hmv1$5z}L)wfwI2HEr*2`%TC1;!1i{^|-gk?u2c9 zYZ9(XsO{zJb9NZeKJuqMo#_cxpWnYr6jOaWod8vDk#N zwC$5O8RlTy#A0bIl?$=(FE(%Bl<_XcGTtm$&btu$-JF%YUr^0E2dj90VGWkob>e8g zwLVrHC)V?|;YPkYJONHQ8BRF^TgqADY;g|kavt`bOPEgp`zGHaULmf;Ql+g;H;S9Y z&Di+$mzF!UWeRK5oqVf!7vJ9b#68%%?!zLrAKOeFHs80!JMwUOfK6S_mUE01eVev7 zV`sh&`|{1&W{k!7e)%94VQmGr?Y}P>`z|(JzGMD~6@J?K%8B4_*VW1Hx4AfG>l7i}JK|d3I5r zV67_0H}9sX6+xe>3Jz1%ghWXY+Ay^)*ozQ{sSV%+V!AGv!aPzb>L#!wxL)l94}yol z!(bQK&9%qC9`F@)d5YK-+%9UtBVadp6g&p@fXBfT;7RaL;0I5Er@=GeS@0Zq9=rfv z1TTTT;AQX%con<`_JP;Ie()x!18;%1!8_nx@E&*{d;mTKAAtb)7<>Z$1wI9zfzQDg z;7jlos0UwzZ@{BZAz&yN z2AEMrjsPRUC@>m~0b{{9kOi_q4#)+0ARmke6Tn0;36MWYUL<*u-$3ix#C`|Z2_6Iw zfrr5^KpiRShL*9NH=4S$mRb+c6XbUpbU7xGEfed zg9=ayR)CeD3RHs{unMdOYrtBt4jc`R0mp*lz&Is0tGr?Kl zY;X?P0?y;wo%BTh0qz2K10T2t+zajl_k&%*S|ow@4!~~kD0mF)0gr>;tcZ{oqYd2i^j2gLlBY;63m@_yBweJ^}&o zG57@h3w#Pb1D}I0z?a}FP!GNa-+*tyci?;Q1NagA6s$!OYLSFmB%u~bs6`TLk%U?# zp%zJ~MG|U}gjyt_7D=c@5^9lzS|p(sNvK5HQwrEF(?70U@=$%mVz?i z0m}fqD3^l@PzhFmm7oe#gBq|3tOnbIdtvpxu=-wkH^)BqkASDaGt7(iEZB#;4!i|E z1e7&WS&LNGB9*mBWv%>%d%p$W!O4^#5?YId)~aZZV?Ybg60`>GIo}fu0F)b2TZ`1z zBDJ+@1lLCb$_nYNMSJW;bL>TH)YI>eRtv!rP|h)VGg3{y6!L*&*CN?IxIcho*CN?k zs`nz*wd!>I>_EbIf(OAv;9;-}yoMjz9olBE+K)fl9a?8ET4yg$0ntdj-y+>;pS@_G zz04r-->Bm+M%pg{+rXvZGH^M#0$d5M0#}1;z_nmI_&c}`Tn}ylH-ekM&EOVrYtSpa zUZrcX$kOuvx2Z0H+)h}3?8|1X1HD+0Kkexm2UQc`aX)+1H*NL4*j zRgYBFBUSZC)n24(FH*G^soINF?M15gB2{~ls=Y|nUU@b(at=6HZ+$m&!r8UEm)hNX zklL-M*B@L$%bC zdD2Vw(@Xc$OZU@D_tQ)F(@W<)c5n81O{Dd3lT>3-zH zgTC~jFFoi>5Bk!BzVx6kJ?Kjh`qG2G^q?<2=t~d!(u2PApf5e>OAq?egTC~jFFoi> z5Bk!BzVx6kJ?Kjh`qG2G^q?<2=t~d!(u2PApf5e>OAq?egTC~jFFoi>5Bk!BzVx6k zJ?Kjh`qG2G^q?<2=t~d!(u2PApf5e>OAq?egTC~jFFoi>5Bk!BzVx6kJ?Kjh`qG2G z^q?<2=t~d!(u2PApf5e>OAq?egTC~jFFoi>5Bk!BzGQYCkPb3HCKwEcfT3U*7!F2& zkzf=U4aR`6U>wK-*&qkxf;^B9#)Ao9BA5ho#NbUpbU7xGEfedg9=ay zR)CeD3RHs{unMdO+Zj8#9^4G<9=M+#xF4q0V=8|1ub-Z`pPsiL-RnX3deFTdbgu{9 z>!IiEq37+P=j}lcd(guk^stAXw}+m$hn}~Gp0|gdw+G$qK{tEQ%^q~K2i@#JH+#^{ z9(vv$dfpy%vL?_~ntA?eM~OcwswQ#Sb@l z&@6tZx9*`I;HS6l*P}M{)II2bKRtCn-x&S3F~E7Sz^y!Qd%+H{6Wj)F2fO$}s}?*0 zc7sR3V_*+>96SM@1pfqn@Dz9&JOiEu&w=N`3*bfY64(n~2Cslu!E0b2cpdBqZ-P4T z7I+)H1KtJif%m}&;6v~c2!M~lC*WV;Q}7x19DD)31Yd!A@HO}bd<(t<--92(kKm`^ zHW*+V46qFb*aib^g8{a|0NZ4bpilM&eL!E(5A+8EKoUsC&p?m@27y$N2GT(W$OMDI z5HJ)B1H-`xFcORcqrn(37K{T~ARFX>T#yIy!FVtMOazm_Wa2*@Oa;@x5nwu)0gC83 zF9OA&1eAisfSxbx;e|cCu!k4+@WLKm*ux8ZcwrAO?BRtyys(EC_VB_UUf9D6dw36E z4@PYbbI=z%D0_UwdFn`hLVErMJ_Vlv>IX*I2BU0)QMSP-UKqs-qj=@_T>k<51cK;W z>P1QR)QNJjk6<4SBgB9fAeM72*|!F5akuAsSN1*G$FolW)SsgM6m_SlJJ@KO8pNLZ zfswYsNZVkfZ7`A-M)JZ)+hC+^DjPpJfO2ZmvBB_5hn{wi%{D4jNeI3wkz`6HKF@ zGz`=5{Z4m!;61>9%ZQ*J`P&ur!6-f$#RsGKU=$yW;)79qFp3XG@xdrQ7{v#p_+S(t zjN*e)d@za+M)AQYJ{ZLZqxfJHAB^IIQG76p4@U99C_Wg)2c!646d#P@gHe1iiVsHd z!6-f$#RsGKU=$yW;)79qFp3XG@xdrQ7{v#p_+S(tjN*e)d@za+M)AQYJ{ZLZqxfJH zAB^IIQG76p4@U99C_Wg)2c!646d#P@gHe1iiVsH7jNyYZd@zO&#_+)yJ{ZFXWB6bU zAB^FHF?=wF561Ap7(N)o2V?kP3?Gc)gE4$Ch7ZQ@!5BUm!v|ydU<@CO;e#=JFoqAt z@WB{97{do+_>kVl(YK}}Z$BS#(^Bd~N_|MF4=L4>sHIR#pq4T%VOo;36#0-MA5!E) zihM|q59#qCG4@EBmKOT1&hxgsE53JX0BXFLg>gJGVB z!H+{%`O#H=bd?`n1;fB_FanGOqrhk|1~Be{uJWU+{OBq_ zy2_8P@}sN#=qf+D%8#z{qpSSrD*f!~M+f=QL4I_QA06bUb^6gkep;s=9ptAa`e})N zTB08v*0r5_@S0n zY_Oi2UI$ikGeXGQIK;4C-N0%XH-ejR?*@;7J-pqa@88mJe~gJsk5jGydYn>Q5q}|- zhOcT4nYwU7-(=GMIO%^9gi;!DKuWRao77^*r{(WuMXbZg(JW9w>Q;c&U=6UJXtv;M z3%#Plk&szPh<4TEVqPa)AzP`JbvJmIf^nPeY86i_8yG!kZO&-l`Yd-4gAW_3E;arB z3?+@v)wtJyP>+y1iP6V1)L!Pp?vBl`2j~ghARZ)uQD8I}1IB`JAPZon7FekTR%(Hj zT41FXj2$taTE}>59pkBWjHlKyo?6FvY8~ULb&RLh3C58GR%*sm>jYM6u?DOK>%h_A z7;r2&4y*@g4aQUJ7*DNZJhhJT)H-nrI1k*)`x;)rNHNxw?O0Q`V@=r(v(%$c>d`0l z=#zT%NxgUs>;aF1C%}{7pTG~E0#Acyz_Z{v@H}_{ya-+bd%?@#74Rx}4eSH2gZvJ_DZvEGg)ddh|&>`lKFxQjb2VN1xQA zPwLSp_2`p&^hrJXq#k`zk3OkKpVXsI>d`0l=#zT%Nj>_cUZ#LSAQhy6bdUiu!C){1 z3?>eD>>d`;-=%0G@Pd)ml9{p2~{;5a*)T4ju(LeR*pL+C9J^H5} z{Zo(rsYn0Rqkpt@OTUw*pM~m>0{u=}otn$BW`YImEAX=dtOjeqI&ciw1kOg{wjw33 zaSwIM^N)U0pq}TSI>sKg{c*d6Ofc?PXJpY1y?{QuW}3Zf9)7eOC{nSB*f$}gtB6xG zKuG#yo5-;JzJq?(LBHqF^c@GJfIDhQm3gm0gmIUTyy$$aC!Y1hv);sGa><*CoEH%$ z_f0J`P52lq3(uy6qsMx6SmcZr0JteWNP0o|v4(m39x&oXaM|Gteq330QvPMQVv zX|v-Dw{tQ;A-43UT zZ`SVl<)0~o8WV3Q1&8$i^N?8A=U48aEga$(NgWi`*@fwiEohU(ENooITV&42BipZFuk!EHf5t!JCvnjBe< zw)dPp2l+0v`JrhnI+bXG-~2mN?IOQ>jTD&nL%yROM%4WLE8Oy%6Ywh@o3{aa5dYC1 zHQ=YDeRg{@siBs28-OV^H`JP1Fq)zjC9N0MbEU>;Av8pzmgWw$(jV*9|E|O~-t?!I)In;Z zF(%{Edf``p{eIQQFM6Ov8=h8dPmL|{7diURmPXA&4cdVeIqqLCor7hod48Jv35DKx z-oE32^G*&!=Yl*#{Wtv@CFajg^)Hn!a%|dB(=D}1uRkQ&O;Zx8S+rlX*4Nb6pWzNY ziLnp0s)m-?zfRj7Jigz2_D^n! zzd7FD>+ul%NEoReZ5C;KL${?FIKZ%Kjer%AH^ROfF= z!C%YD4cW1uNS!gEzo^v&d%;=5K#h)8yX>D6D_1$&D>#h5?QZblc>IfZ&N$$oY}Yt2 z(IMV!J_2B$|M~X7yZ?-%PzsudWpl-UA_Y3;2F7Tfj8N;Kjlgb^_A&uCYNZY4kOv;F1O^2Z}MM1SMo51f(1 zU*uEgNv!bS54GtXx|R=Q6DM3f%JGZOcTGdU-!G0n9O&;*N53_$Lxu6%uKilOCO?0X z2LEm8)4rOgp!xA{59vSY^U!tp*XzcAuapkeYVDAk$(~{*F+J0!;N74t?9XGCyK|W5 z?tNyw`%p%T{j65kUj&&;vw#)u7O;-nSXOU4jXCYk;9n-s;_uOOX|lfCdHlgy^XmvS;6f=)*784t+3t4BGxdy`gv5-Rtl{=F z>zn7zk}Y#*?08FON>R@sg68&)Xu%AZt}yn~g;V&p?AR>jJP z^?aLh7qjGbkhQ9_>MS4CE04*?Sa+<4e4LfX-0}(59P1~aWEQ;v@+sCF8z!G-b+IhD zPvxjQ`3`gIjhFAUqSzGq0rTq}E5SkJO{L{>=eCf=~_X(?| zeae3n^YVSgI%!|?AII!`5h}~-Vf9eidY(R&qgTFCdCZNe=UD8*teJYvt}xNf{JS&L zVkC1dCUU(O|1f4YWRYg}#V9lXVhd(qT*w+=rOctI#EJaF z%v_4ynbqPH&YUVvXLYbMXj^S*Tjz0RE30O8W^To+SRdZ00IbZdxukZ1*T`e}qVza}en%*(7K7%pFxuaTa8av%4;E??*T8}beA;_Xw;e8`%CN`Azufh}dg z%yRp&{Fw6jTz*aod?CN0#Ohg9Hx6n1jx($pND2QWgS_2IMf2__%VzQhCO_Q#MJ#gR z;tcBt@@{8@ir{=C>j=iNf?yPPA<1l!WVU+dTo;ne8gocWd+zO^I^Z9v771o9U5hmZ zGe(UgoGg{anH-hFHdp0xSDwn_Jggyl!Wt7rrkbQC z;hwA}i(afrI7JNCJR;oi$TTsS`8to_>P$70t9ou1~Ymt6hoNp_ee2-wF{5p-a=K#y~U~+KP9RJ|D~#wtMC!-Dpf@ZRI6$+iWLlN zNGIGR@-#PzEX_?KA8tBZV#e~R?i&Dd#tyq+eDJ)GLZ(C-OU-F@`=9c9(5029o(z##eJW;5BL4* ze%ueJ2XH^AcHwh3^W}7e`5qUkn)}$oePW>IKDMl0C`1W9*+DDzf z&Md}}ng>N&&4ak#R&R^`W=7*kGox_}RyBNIq%dFU2ciS38-B>!vH=yKE$KB5yKAoG z>KEz@-u~6|9ml|$UlA9)Dbh4=iX=0$scy*k!7#KlW~5EvNHd@^4A=wKI}x|dbeiL0 zn{Zq-9Cr&0tJy8auv<&RZc&EaI>T zu}y}d?Afd>!%ddqCaseTMMt=4HIl4ZDblb~8^cO13@demmCho3R!4;sH6OVQ2emZ3 z<8pXMGmbKh6WxSyTC%F?9WdQLx)VH`a(ccf^;ilr@hdt7skg;s8fTQg1=E0?zAjOLxz zhIgc1!xUc7yc6f}j%J)T&P?8#cOuPv-WF?`CUTxtO$mWjP1(}RfFm>qMH=?84DYmY z7>CtOxvIIQjp3Rw!!^=yO{B_(Wh}!lk%nJ78D{BZI3>++N>8QPq?2KjRKq4c4Vw%! zY?5NwB;K$|Ps1b0hCw>PAd8TJMg|#V7{qNDB;L#^p1^vj%aGV|<~nI-<`?g6<`?h7 z>ZmK=1wG4nSHm)c4a+1k*YGO*>v_l9skLe?T&8CqxAe-XoHUz#> zR2y+0ua1WiPEaQi+R5rv&TLYfxcWEsH_mTXo8kJ?)M@bA>C9i=$MDxU)>u6Y#?&k} z*09(Z!(y4Nxq2>)skv;V;j$5i%hC;(4KZBSR|KHEW)R z*(}4_qzR%6tCRMkSLUKOF6m`SRzXcbs&7&gU>QB1`ebK5^)6;U^{$-F!k689U%+0^ zlRw|glV3?CHl*7M|VH1p)2VdlwyL(h}Xiqy=LFFV34{b3sB$%ko}C!e*VnJ1t1 zqM0Y3Bi*Y%TF;QrddwFwQ}ud1L%!UkXUK<9m?0lV(Y^XDrdNNimVEp%L%zJ&^ye=z z{rPQ3en4KXXULcL>KXFo{m5##+=CoO(f8Ip_Ggi|&hj}eUGiluSMn7tQSwziLq0w8 zzN)W$9a&0}Z|E8F<(qnjeEFl&tNdEJ-6P-3kk6{bdZph8tvTsY9EEm_R(gfs*1An2 z+02tqOw5yyWYdD_QP5tIZ(6GAq+2Ia&OG_5i|J|i*7b@UGfzG%6f;jgDPW#_`r)7R zAEawkrRsU|=}9CAk;ofIiM_x8kP3!?Q4X@%PXLF5nP46$1m$3{ki9%YB@6(mUiE@$0u-Rm94c8?sV(!23~YR_Rk z#|Y5@>x=E`NC(Y1_e02~&vi8ZIz~2}YuVsx-{5jLxVkpDIOp7(+;FZ>gRAiz>#W=H zcDu8|Mf#k3Nu!g#+y>XA2G_KZt1(9%XNS&pT+rZJ6moUDSBP#83w|A!hFm>$TFh$i zJp;HtH&-hfT#PK+=k(Q%N1Lk~L+3i4^8fm}8sMtR^!$D2WYfs@J@ zT52gvEdv-+gb?T@s|?FRB0r)N0}XL?SxXtfz(UMM3|T}Lfnb)hR)sPQ%QBRumPHpB z77!U0nT%zuqZAp}I>=bk=Y7ukzH@PDlKI~I-1qN$zxVq&=ia=z*P`{1ZbaKs9;22& z=6T0pQ4wGa8KLFmPk84Nvt zXp!U~EIu5g#8AuI z0==ODW*{-bqWdfwW6?N+Jj{M%QE89|FNrF8$s1l`hDEb2nrDz$L|&dS$ir&#u#Vts zu|a|44=WxNDDeYIS0vUXTJfws@vKEL4=7nnspSPZ*kfm)C&^Wqp-mQTHAtk1F40T8 zW_UsWU{>&GurO#y78{hdt_FDvA;sdcrykmK9Srqe58J>5=pu{G4zs*_4f4if9aK7G1XJV}s%|J6DD5U|+-bnnhn26f8AJ^panRUdl1NREb6XEy7L? z{RPkqpaF}tMLCPGV?(Lm0kqFYyy-m_tuQDx+@Lgg>HVUY8fkc`(H3F7L|$-;MhNFf zgw|LDUg#yg$@1DQig{ZN&u_B`kw&(M3@`nbMW-w}XHY!06jpi|OKOrq8RRD9`J+&Z zem}=H&7y}>vr>goWtD2O2s0D%M4DPoX|v&_S`6}!r`G%DQyWuTZi}#I z{E2=wJnXP&mql+{bkL%G7J--QrtLAyJ7Q7HJ85}mExKTkj|d_!7-1C46^lNz=(<5- z5$WADC|v|J)2~(PPGs^`nkgS{tVQoYR>mWh{Eh;z2FNS?zw9*)*J;Tdx zh2CpftkyLAM`(yeLoFI%(R~(Ki`a{x^f>yfw0cz*HBxKsew#t*E{k3>NFs>*9WuQ1TL#@O zzTPb|%GjpgvFMaV=L{~^o{ff2E|@7#l|97j{Vylv;^z@!hhT4I4oF`7kI}9%Lx(LoYSD3vP8*bc7Gv2Y7Fq1N%vZEM zZ_y=#{Otx&?=tn!TV#PgL*#EZDD#CuS)3MLfA%rMAkVR=#G*H`!uZ-Wz8A=riyfl# zyx|rdgC1Yccx4u)E!q+4;hNtEZJ<2!a)wuc?yhh-In*EMfSBzJaUgSAR69vt9R3&Y>^H zpw{f{41?m6TCQN>Pka^!-B>fsO~G{#J30K3y&56A&;5Htj=wj%!b;a5o>qBQ;IAFL z@MPEu#CwZumqq)54v9tft?WD5Q`vLbiv^K>AQH$v@^K2tkN;Ic4n7Zg*&A_hAqRRY zj;{a}L(&(mwBTOAX174E10&#ld%+#@DA@5nHl8isukqf@UtoK#haTrh&Rt;4k(~Sy zvRCnVlycUEKXN)oXs1Q5N1VO!8uE`?slA5AXLg(`(fayHNFGnU_*D#fI3o(rCgFvr zMZtR+Iq2L&NjapRAaJ7h8RF9{KEuo@0IlI@BSQ-yp3k&qEJTbzP}pyQ=PK}`HEpE9 zQo{@E-Vf~F53u*cp8%&`gu4Iu9wgXhr8r9>z1U*oZFsT84l9j$aUZe2-s8K1aegMA z;#C}<-obU-2hO4Jy|nmLj^7n3$o&w>kXYqLpz*Z1-F)O`Ksq}oXMxr)y1xUhE!rT| z{U!0glFB#M))p-XT~o9U^grBJL7R)($q(tb1^3(yI^6v)(8t{y!g0SOJx2N-=!-=! zgVwpf!rwk#WsiR#J?zR~>-P7^H{ae(|A*aw1E-n%X1AYUiBp}_tAY2?Y9-@bNu6IX zwvTB0&x~zlL?yPB?z^Poa}ncgVT3Cg;YwE`eBM0(`W^Q59p>Z^{T~wl&Y#lrZi!GI zlGvQV!co7GNL0IMsxQfDFG7#tKnI_zhMcHc$%*=z^awt30*W3%B@g51d8&9;N9pZF z#_%-hLmc(X9QE%w>Mb1aHb%0OF`r#D+g1$$EH?{@*Kl3b>h^>Fj+vW1B7`)zfJ;of%tAof_)YFv4Flo=N0P zB4-L|r^M`(P})g9ot&d{^w7x&7cs)u7~xq)*vY;+IbWTOq0^Oh^)C{qvpeKSB&Zz0 zcL(cwa&R8AFO0|;!yeydU)$(^8GGDKeii#okpErwTgAQ}W*$1JxtL>F%=MXfe}P`| z?%P69$RgR0?}*Mj{|)+_yBj@z-@)wbf5cyVwMsLdr#PpzP70jg;4=qBDms(o`;+k* zak9qVKve`d4%V&HC06)5EcEr4)Ni8xc+@~>J)TyNyW4=fX!V%8i~NxOE$}$!6XA$A zeD0j=(|*qXf=my-st=%==-Q2()fM>`4;XY z>dyr0{-EDy24=bIfFGpiQs$wQF|T8Ur7~V!%5kmX%AZKh!PFc~ogXpsq+s_J7NWQ%^uFzGICzxj+jHv?@4Py zN~^yoo+=n;ovghJqE4dTf<4g5l6>jJmyGxQ^SVGwniNNXc1 zJEf8MD*dk{-Xs+I(tC9jznu1ah)4a3erpf>JAWhk&K?IVA602``u$Y>xnNz2-iL}*b?GwT1UgbaGWf?Wtjgwb@KF3Mx8Qp9^@|Yi36$r)cvW`pRu))Uq?S(k`rAg zy*Rb>`DM=za2lxJz}`KPfv#j&Xz}u8gMSYRhTo1Z}oJpi(sehR9??v%+$ojZYxA=l6X7xg)PK|Oo}y;j8-UxoXiTged?LypK) z4h=mj^r+AyGNKh%p_11!8s9H3^cB)0q}`y!sE00teGaz6#e!%1g`P^_t-QXxLVDz8 z4>;XDQ*2ZyB1B9>Ku2l}j8#M9qpG0+%=bWWooGhpxC_((CD(&T(H5e~&S^qhincsz z&3J4 zq}h!6^PQ;A+{@Tx7jEWRyIM|n^`h))+!M*Y#yxU%Rlk<&r#d3nbG1gU&+6mw`YflH zdReZwYA5fQ&hQT6X}QBvXXIWI6!((qjND7A^Kvh#ZpeL*IxY7;>T}YIpk`%>UbwPK zF*le|7xxm#BhC|40m-yVF7aH?bEYY$qOz(+^{=mMYQS140$=s?MC;SV^KW1+4ZxcH z7Ao-XQzhePKA2P04~(zLsV5%DgEmhdKQpJcPnlYkQ-|_Xr{vUy8j*fclb@W!sbjoh zEq%?KNWO{guzx9$Rcm_sy6jJSk(Kl!yQ3FbpI&6&>_yhM7g@go*`T_*`X<#hckYvm z)w854<~1yOQmtLkST|R-)j$4No$9DxG`C)DCf!cDlk|1cy`%?750f5kc#WK^0U?A7R0#d|a0eTy_f%4;lEjqq_Af`+pZ zo=ta<)0ea#>0nR?s{p4abY<7e|9}V`h_hxC_C*&?&TG0@&qKA}cGmWt*XH*MDruWg z(|yTRZrUB;HoA?l6zdXr9fto(kow~F>Lom;^$VyYtaU1#&Cbu9EzT>>Cg)}6B~*xO z^siAj-G-Co-%+DBTtA}c=udGKZ9^sJdQ@<3K+WcEtfvPuT%+!~u=27Rx_!8xqU(V> z7rsWh^FaT~oew$|XQ$)dk6w(%9BR~3qmCMnQUl)I(e4-<{auJeBT6l=XZfh`-z$hh z2UarFN=ih6|FSW;#+8OwSiCm2JO}e%#M~q6;7YyVM#-$z5Ua3{J8t3ilbj&?a<FiK0D*Zd0?R;GCY~y1a?6L2iS0ia>D3utTu8>Fk^8)-6 z%rxftYx$7t98Mchr3Qf_F4V|Nrewv)yON*P9c4%MBqlfG6 zP~8Hp9;~M4`55SRh_Xkwa)tc_G!P%C_b>E|`)SPnryNHZpMpR6@s#uQV+)%^`TqbQ CkmAn( literal 0 HcmV?d00001 diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 4b91cba36..77e2c2444 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -147,6 +147,7 @@ "theme": "Theme", "themeDay": "Light", "themeNight": "Dark", + "themeSpringSream": "Spring Stream", "types": "Types", "zipExport": "Export for web", "zipProject": "Pack project to .zip", diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 3f12e5832..71bd7bad6 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -126,6 +126,7 @@ "theme": "Тема", "themeDay": "Светлая", "themeNight": "Тёмная", + "themeSpringSream": "Весенний ручей", "types": "Типы", "zipExport": "Экспорт в .zip", "zipProject": "Упаковать проект в .zip", diff --git a/app/package-lock.json b/app/package-lock.json index 39e533e0a..b14316752 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "ctjs", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2756,7 +2756,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -2764,7 +2764,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } @@ -3531,7 +3531,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { diff --git a/package-lock.json b/package-lock.json index edffa50f7..df9c56192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,7 +769,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -4262,7 +4263,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "micromatch": { "version": "3.1.10", @@ -8737,7 +8739,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, diff --git a/src/js/codeEditorHelpers.js b/src/js/codeEditorHelpers.js index d70f05cf1..3f4f7f5cb 100644 --- a/src/js/codeEditorHelpers.js +++ b/src/js/codeEditorHelpers.js @@ -226,6 +226,7 @@ Day: 'tomorrow', Night: 'ambiance', Horizon: 'horizon', + SpringStream: 'spring', default: 'tomorrow' }; const glob = require('./data/node_requires/glob'); diff --git a/src/node_requires/monaco-themes/spring.json b/src/node_requires/monaco-themes/spring.json new file mode 100644 index 000000000..2dc3f234e --- /dev/null +++ b/src/node_requires/monaco-themes/spring.json @@ -0,0 +1,215 @@ +{ + "base": "vs", + "inherit": true, + "rules": [{ + "foreground": "8e908c", + "token": "comment" + }, + { + "foreground": "666969", + "token": "keyword.operator.class" + }, + { + "foreground": "666969", + "token": "constant.other" + }, + { + "foreground": "666969", + "token": "source.php.embedded.line" + }, + { + "foreground": "c82829", + "token": "variable" + }, + { + "foreground": "c82829", + "token": "support.other.variable" + }, + { + "foreground": "c82829", + "token": "string.other.link" + }, + { + "foreground": "c82829", + "token": "string.regexp" + }, + { + "foreground": "c82829", + "token": "entity.name.tag" + }, + { + "foreground": "c82829", + "token": "entity.other.attribute-name" + }, + { + "foreground": "c82829", + "token": "meta.tag" + }, + { + "foreground": "c82829", + "token": "declaration.tag" + }, + { + "foreground": "c82829", + "token": "markup.deleted.git_gutter" + }, + { + "token": "number", + "foreground": "008bad" + }, + { + "foreground": "009170", + "token": "constant.numeric" + }, + { + "foreground": "009170", + "token": "constant.language" + }, + { + "foreground": "009170", + "token": "support.constant" + }, + { + "foreground": "009170", + "token": "constant.character" + }, + { + "foreground": "009170", + "token": "variable.parameter" + }, + { + "foreground": "009170", + "token": "punctuation.section.embedded" + }, + { + "foreground": "009170", + "token": "keyword.other.unit" + }, + { + "foreground": "c99e00", + "token": "entity.name.class" + }, + { + "foreground": "c99e00", + "token": "entity.name.type.class" + }, + { + "foreground": "c99e00", + "token": "support.type" + }, + { + "foreground": "c99e00", + "token": "support.class" + }, + { + "foreground": "ce5a24", + "token": "string" + }, + { + "foreground": "ce5a24", + "token": "constant.other.symbol" + }, + { + "foreground": "ce5a24", + "token": "entity.other.inherited-class" + }, + { + "foreground": "ce5a24", + "token": "markup.heading" + }, + { + "foreground": "ce5a24", + "token": "markup.inserted.git_gutter" + }, + { + "foreground": "3e999f", + "token": "keyword.operator" + }, + { + "foreground": "3e999f", + "token": "constant.other.color" + }, + { + "foreground": "4271ae", + "token": "entity.name.function" + }, + { + "foreground": "4271ae", + "token": "meta.function-call" + }, + { + "foreground": "4271ae", + "token": "support.function" + }, + { + "foreground": "4271ae", + "token": "keyword.other.special-method" + }, + { + "foreground": "4271ae", + "token": "meta.block-level" + }, + { + "foreground": "4271ae", + "token": "markup.changed.git_gutter" + }, + { + "foreground": "8959a8", + "token": "keyword" + }, + { + "foreground": "8959a8", + "token": "storage" + }, + { + "foreground": "8959a8", + "token": "storage.type" + }, + { + "foreground": "c82829", + "background": "c82829", + "token": "invalid" + }, + { + "foreground": "c82829", + "background": "8959a8", + "token": "invalid.deprecated" + }, + { + "background": "ce5a24", + "token": "markup.inserted.diff" + }, + { + "background": "ce5a24", + "token": "meta.diff.header.to-file" + }, + { + "background": "c82829", + "token": "markup.deleted.diff" + }, + { + "background": "c82829", + "token": "meta.diff.header.from-file" + }, + { + "foreground": "3e999f", + "fontStyle": "italic", + "token": "meta.diff.range" + } + ], + "colors": { + "editor.foreground": "#555", + "editor.background": "#FFFFFF", + "editor.selectionBackground": "#D6D6D6", + "editor.lineHighlightBackground": "#EFEFEF", + "editorCursor.foreground": "#AEAFAD", + "editorWhitespace.foreground": "#D1D1D1", + + "errorForeground": "#dd3b98", + "menu.selectionBackground": "#00c09e", + "menu.border": "#d6dedd", + + "textLink.foreground": "#00c09e", + "editorLink.activeForeground": "#00c09e" + } +} \ No newline at end of file diff --git a/src/pug/index.pug b/src/pug/index.pug index fc0b22061..88c176adf 100644 --- a/src/pug/index.pug +++ b/src/pug/index.pug @@ -57,10 +57,12 @@ html const tomorrow = require('./data/node_requires/monaco-themes/tomorrow.json'); const horizon = require('./data/node_requires/monaco-themes/horizon.json'); const ambiance = require('./data/node_requires/monaco-themes/ambiance.json'); + const spring = require('./data/node_requires/monaco-themes/spring.json'); window.monaco = global.monaco; monaco.editor.defineTheme('tomorrow', tomorrow); monaco.editor.defineTheme('horizon', horizon); monaco.editor.defineTheme('ambiance', ambiance); + monaco.editor.defineTheme('spring', spring); window.signals = window.signals || riot.observable({}); window.signals.trigger('monacoBooted'); }); diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index bc27bb740..faee29155 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -322,7 +322,13 @@ main-menu.flexcol this.switchTheme('Day'); } }, { - label: window.languageJSON.menu.themeNight, + label: window.languageJSON.menu.themeSpringStream || 'Spring Stream', + icon: () => localStorage.UItheme === 'SpringStream' && 'check', + click: () => { + this.switchTheme('SpringStream'); + } + }, { + label: window.languageJSON.menu.themeNight || 'Night', icon: () => localStorage.UItheme === 'Night' && 'check', click: () => { this.switchTheme('Night'); diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index f6f8153fd..6e87cc0f7 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -17,6 +17,7 @@ project-selector background rgba(white, 0.9) flex-flow column nowrap z-index 35 + {shadamb} .inset margin 0 -1rem -1rem .menu diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl new file mode 100644 index 000000000..78b1c1a25 --- /dev/null +++ b/src/styl/themeSpringStream.styl @@ -0,0 +1,101 @@ +@charset "utf-8" + +// минимальная ширина шаблона - 380 пикс + +/* частотники */ +trans = + transition 0.35s ease all +transshort = + transition 0.15s ease all +shad = + box-shadow 0 0.1rem 0.35rem rgba(0, 0, 0, 0.25) +shadamb = + box-shadow 0 0.25rem 1rem rgba(0, 0, 0, 0.15) + +/* шрифты */ +fonts = font = 'Open Sans', sans-serif, serif +fontsHeaders = fontHeader = Comfortaa, 'Open Sans', sans-serif, serif +font-mono = mono = Iosevka, monospace + +br = 0.35rem +iconsize = 1.5rem + +/* цвета */ +act = #00c09e +acttext = #009170 +accent1 = #00c09e +accent2 = #008bad +error = #dd3b98 +red = error +success = #009170 +green = success +warning = #ce5a24 +orange = warning + +theme = 'Spring Stream' +themeDark = false + +.error + color error +.success + color success +.warning + color warning + +bl = #d6dedd +bd = #d6dedd + +text = #555 +snow = #fafafa + + +@require 'hvost.styl' + +@require '3rdParty/*.styl' +@require './../../app/node_modules/highlight.js/styles/tomorrow.css' + +@require 'common.styl' +@require 'inputs.styl' +@require 'typography.styl' +@require 'confetti.styl' +@require 'buildingBlocks.styl' +@require 'tabs.styl' + +@require 'tags/**/*.styl' + +h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, button, .button + font-family fontsHeaders + font-weight 300 + + +h2, .h2 + font-weight 600 + +h3, h4, h5, h6, .h3, .h4, .h5, .h6 + font-weight 900 + +button, +input[type="button"], +input[type="submit"], +input[type="reset"], +.button, +.selectbox + font-weight 900 + border-width 0 + background accent1 + color white + svg + color white + &.selected + background acttext + +#bg + background #f2fcfa + +.cards + box-sizing border-box + padding 0.25rem + gap 0.75rem + li + {shad} + border-color white \ No newline at end of file From 994ced27c5ec993d5ac7586ce817f790528db18a Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 09:06:14 +1200 Subject: [PATCH 12/86] :bug: Fix blank autocompletion list at room-events-editor Closes #195 --- src/riotTags/rooms/room-events-editor.tag | 2 +- src/styl/tags/rooms/room-events-editor.styl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/riotTags/rooms/room-events-editor.tag b/src/riotTags/rooms/room-events-editor.tag index 0b0e5a4cf..86b78f3dd 100644 --- a/src/riotTags/rooms/room-events-editor.tag +++ b/src/riotTags/rooms/room-events-editor.tag @@ -1,4 +1,4 @@ -room-events-editor.view.panel +room-events-editor.view .tabwrap ul.tabs.nav.nogrow.noshrink.nb li(onclick="{switchTab('roomcreate')}" class="{active: tab === 'roomcreate'}" title="Control-Q" data-hotkey="Control+q") diff --git a/src/styl/tags/rooms/room-events-editor.styl b/src/styl/tags/rooms/room-events-editor.styl index 41a09b360..1bd3c908f 100644 --- a/src/styl/tags/rooms/room-events-editor.styl +++ b/src/styl/tags/rooms/room-events-editor.styl @@ -1,4 +1,5 @@ room-events-editor + @extends .view border-top 0 none display flex flex-direction column From b4045c2602a77950273bb7ef217144691c46265e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 10:46:59 +1200 Subject: [PATCH 13/86] :zap: Separate types' extensions editor to a tag extensions-editor --- OurRiotDocFormat.md | 44 +++++++++++++ src/js/gulpWatch.js | 2 +- src/riotTags/shared/curve-editor.tag | 18 +++--- src/riotTags/shared/extensions-editor.tag | 78 +++++++++++++++++++++++ src/riotTags/type-editor.tag | 49 +------------- src/styl/common.styl | 2 - src/styl/inputs.styl | 3 + 7 files changed, 137 insertions(+), 59 deletions(-) create mode 100644 OurRiotDocFormat.md create mode 100644 src/riotTags/shared/extensions-editor.tag diff --git a/OurRiotDocFormat.md b/OurRiotDocFormat.md new file mode 100644 index 000000000..cd0f766aa --- /dev/null +++ b/OurRiotDocFormat.md @@ -0,0 +1,44 @@ +Maybe we will write a parser that outputs readable docs for these. + +At `./src/riotTags`, use this syntax at the top of the file to document tags: + +```pug +// + Short description of a tag. + + A longer description, if appropriate. + + @slot + This means that the tag yields the passed markup inside itself. + A description of how the tag does it would be appropriate. + + @attribute attributeName (type) + A descriotion of an attribute. + @attribute [optionalAttributeName] (type, typeSpecificator) + A descriotion of an attribute. + + @method callableMethod(x, y) + A description of a tag's method that can be safely called for inter-module communications. + @method callableMethodWithoutArguments + A description of a tag's method that can be safely called for inter-module communications. + @method callableMethod(x, y) (type, typeSpecificator) + A description of a tag's method that can be safely called for inter-module communications. + + @property propertyName (type, typeSpecificator) + A descriotion of an exposed property of a tag. +``` + +Optional attributes are inside square brackets. + +A `type` is one of: + +* `riot ${type}`, where a `${type}` is a TypeScript spec of a field, usually a mutable object. E.g. `riot function`, `riot Array`. Here `riot` means that the value should be passed as a riot attribute in form of `attribute="{dynamicValue}"`. +* a constant's type, e.g. `string`, `number`, `boolean`. +* `atomic`, for attributes-switches, when the existence of an attribute is more important than its value (think of `disabled="disabled"` attribute in HTML). + * in 99.9%, `atomic` attributes are optional and need square brackets. + +A `typeSpecificator` is a more detailed notation of a type in TypeScript fashion, e.g. for a string, it may be `'left'|'right'|'top'|'bottom'`. So the whole line may look like this: + +``` +@attribute direction (string, 'left'|'right'|'top'|'bottom') +``` \ No newline at end of file diff --git a/src/js/gulpWatch.js b/src/js/gulpWatch.js index 2792cfe24..066183416 100644 --- a/src/js/gulpWatch.js +++ b/src/js/gulpWatch.js @@ -5,7 +5,7 @@ const reload = () => { if (!reloading) { reloading = true; - nw.Window.get().reload(); + nw.App.quit(); } }; gulp.watch(['./data/theme*.css', './index.html', './data/bundle.js', './data/js/**.js', './data/node_requires/**/*.js'], reload); diff --git a/src/riotTags/shared/curve-editor.tag b/src/riotTags/shared/curve-editor.tag index c5ee725f7..a59dbc853 100644 --- a/src/riotTags/shared/curve-editor.tag +++ b/src/riotTags/shared/curve-editor.tag @@ -10,30 +10,30 @@ Called when a user drags a curve's point. Passes the whole curve and an edited point as its arguments. - @attribute lockstarttime (atomic) + @attribute [lockstarttime] (atomic) Locks the time of the firts point in the curve. Also, it forbids the deletion of this point. - @attribute lockendtime (atomic) + @attribute [lockendtime] (atomic) Locks the time of the last point in the curve. Also, it forbids the deletion of this point. - @attribute lockstartvalue (atomic) + @attribute [lockstartvalue] (atomic) Locks the value of the first point in the curve. Also, it forbids the deletion of this point. - @attribute lockendvalue (atomic) + @attribute [lockendvalue] (atomic) Locks the value of the last point in the curve. Also, it forbids the deletion of this point. - @attribute timestep (number) + @attribute [timestep] (number) A step size for a manual point editor. Defaults to 0.01. - @attribute valuestep (number) + @attribute [valuestep] (number) A step size for a manual point editor. Defaults to 0.01. - @attribute type (string, 'float'|'color') + @attribute [type] (string, 'float'|'color') Defaults to 'float'; if set to 'color', requires an attribute `colorcurve` to be set and allows for color editing. - @attribute easing (string, 'linear'|'none') + @attribute [easing] (string, 'linear'|'none') Defaults to 'linear'. - @attribute coloreasing (string, 'linear'|'none') + @attribute [coloreasing] (string, 'linear'|'none') Defaults to 'linear'. curve-editor(ref="root") diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag new file mode 100644 index 000000000..b58928314 --- /dev/null +++ b/src/riotTags/shared/extensions-editor.tag @@ -0,0 +1,78 @@ +// + The tag shows editable extends for a given asset type. + Will be an empty inline element if no suitable extends were found. + + @attribute entity (riot object) + An object to which apply editing to. + @attribute type (string, 'type'|'tileLayer') + The type of the edited asset. + + @attribute [compact] (atomic) + Whether to use a more compact layout, replacing full-text hints with icons + and using more compact classes for fields. + + @attribute [customextends] (riot Array) + Instead of reading modules' directory, use these extends specification instead. + Useful for quickly generating markup for built-in fields. + +extensions-editor + virtual(each="{ext in extensions}") + label + div(class="{parent.opts.compact ? 'flexrow' : 'block'}") + input.nogrow( + type="checkbox" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + if="{ext.type === 'checkbox'}" + ) + b.nogrow {ext.name} + b.nogrow(if="{ext.type !== 'checkbox'}") : + .filler(if="{parent.opts.compact}") + hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") + input.wide( + class="{compact: parent.opts.compact}" + type="text" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + if="{ext.type === 'text'}" + ) + input.wide( + class="{compact: parent.opts.compact}" + type="number" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + if="{ext.type === 'number'}" + ) + .dim(if="{ext.help && !parent.opts.compact}") {ext.help} + script. + const libsDir = './data/ct.libs'; + const fs = require('fs-extra'), + path = require('path'); + + this.mixin(window.riotWired); + + this.extensions = []; + this.refreshExtends = () => { + if (this.opts.customextends) { + this.extensions = this.opts.customextends; + return; + } + + this.extensions = []; + + for (const lib in global.currentProject.libs) { + fs.readJSON(path.join(libsDir, lib, 'module.json')) + .then(moduleJson => { + const key = this.opts.type + 'Extends'; + if (key in moduleJson) { + this.extensions.push(...moduleJson[key]); + } + this.update(); + }); + } + }; + window.signals.on('modulesChanged', this.refreshExtends); + this.on('unmount', () => { + window.signals.off('modulesChanged', this.refreshExtends); + }); + this.refreshExtends(); diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 4436fecfd..769066209 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -11,29 +11,8 @@ type-editor.panel.view.flexrow b {voc.depth} input#typedepth.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") .flexfix-body - virtual(each="{extend in libExtends}") - label.block - input.wide( - type="checkbox" - value="{type.extends[extend.key] || extend.default}" - onchange="{wire('this.type.extends.'+ extend.key)}" - if="{extend.type === 'checkbox'}" - ) - b {extend.name} - span(if="{extend.type !== 'checkbox'}") : - input.wide( - type="text" - value="{type.extends[extend.key] || extend.default}" - onchange="{wire('this.type.extends.'+ extend.key)}" - if="{extend.type === 'text'}" - ) - input.wide( - type="number" - value="{type.extends[extend.key] || extend.default}" - onchange="{wire('this.type.extends.'+ extend.key)}" - if="{extend.type === 'number'}" - ) - .dim(if="{extend.help}") {extend.help} + extensions-editor(type="type" entity="{type.extends}") + br br docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") .flexfix-footer @@ -79,30 +58,6 @@ type-editor.panel.view.flexrow this.getTypeTextureRevision = type => glob.texturemap[type.texture].g.lastmod; - const libsDir = './data/ct.libs'; - const fs = require('fs-extra'), - path = require('path'); - this.libExtends = []; - this.refreshExtends = () => { - this.libExtends = []; - for (const lib in global.currentProject.libs) { - fs.readJSON(path.join(libsDir, lib, 'module.json'), (err, moduleJson) => { - if (err) { - return; - } - if (moduleJson.typeExtends) { - this.libExtends.push(...moduleJson.typeExtends); - } - this.update(); - }); - } - }; - window.signals.on('modulesChanged', this.refreshExtends); - this.on('unmount', () => { - window.signals.off('modulesChanged', this.refreshExtends); - }); - this.refreshExtends(); - this.type = this.opts.type; this.tab = 'typeoncreate'; diff --git a/src/styl/common.styl b/src/styl/common.styl index 022408895..25683e2c1 100644 --- a/src/styl/common.styl +++ b/src/styl/common.styl @@ -56,8 +56,6 @@ body .short width 4em - padding 0.2em 0.3em !important - line-height 1 !important // прогрессбары .progressbar diff --git a/src/styl/inputs.styl b/src/styl/inputs.styl index cafd2b868..5bd51a080 100644 --- a/src/styl/inputs.styl +++ b/src/styl/inputs.styl @@ -37,6 +37,9 @@ textarea {transshort} &:focus border-color acttext + .compact + padding 0.2em 0.3em + line-height 1 input[type="number"] padding-right 0 From 28421291ec80c844561fdc38eabf50624f0cf304 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 11:17:02 +1200 Subject: [PATCH 14/86] :zap: Lint ct.release --- app/data/ct.release/.eslintrc.json | 3 +- app/data/ct.release/camera.js | 131 +++++++++++------ app/data/ct.release/emitters.js | 30 ++-- app/data/ct.release/inputs.js | 12 +- app/data/ct.release/main.js | 52 ++++--- app/data/ct.release/res.js | 21 +-- app/data/ct.release/rooms.js | 70 ++++++---- app/data/ct.release/sound.js | 217 +++++++++++++++-------------- app/data/ct.release/styles.js | 68 ++++----- app/data/ct.release/timer.js | 15 +- app/data/ct.release/types.js | 82 ++++++----- gulpfile.js | 2 + 12 files changed, 404 insertions(+), 299 deletions(-) diff --git a/app/data/ct.release/.eslintrc.json b/app/data/ct.release/.eslintrc.json index 8a5b17a2f..d2e1c1a5b 100644 --- a/app/data/ct.release/.eslintrc.json +++ b/app/data/ct.release/.eslintrc.json @@ -9,6 +9,7 @@ "Camera": false }, "rules": { - "prefer-destructuring": "off" + "prefer-destructuring": "off", + "spaced-comment": "off" } } diff --git a/app/data/ct.release/camera.js b/app/data/ct.release/camera.js index e4b733137..dc49e5471 100644 --- a/app/data/ct.release/camera.js +++ b/app/data/ct.release/camera.js @@ -7,29 +7,55 @@ * @extends {PIXI.DisplayObject} * @class * - * @property {number} x The real x-coordinate of the camera. It does not have a screen shake effect applied, as well as may differ from `targetX` if the camera is in transition. - * @property {number} y The real y-coordinate of the camera. It does not have a screen shake effect applied, as well as may differ from `targetY` if the camera is in transition. - * @property {number} width The width of the unscaled shown region. This is the base, unscaled value. Use ct.camera.scale.x to get a scaled version. To change this value, see `ct.width` property. - * @property {number} height The width of the unscaled shown region. This is the base, unscaled value. Use ct.camera.scale.y to get a scaled version. To change this value, see `ct.height` property. - * @property {number} targetX The x-coordinate of the target location. Moving it instead of just using the `x` parameter will trigger the drift effect. - * @property {number} targetY The y-coordinate of the target location. Moving it instead of just using the `y` parameter will trigger the drift effect. + * @property {number} x The real x-coordinate of the camera. + * It does not have a screen shake effect applied, as well as may differ from `targetX` + * if the camera is in transition. + * @property {number} y The real y-coordinate of the camera. + * It does not have a screen shake effect applied, as well as may differ from `targetY` + * if the camera is in transition. + * @property {number} width The width of the unscaled shown region. + * This is the base, unscaled value. Use ct.camera.scale.x to get a scaled version. + * To change this value, see `ct.width` property. + * @property {number} height The width of the unscaled shown region. + * This is the base, unscaled value. Use ct.camera.scale.y to get a scaled version. + * To change this value, see `ct.height` property. + * @property {number} targetX The x-coordinate of the target location. + * Moving it instead of just using the `x` parameter will trigger the drift effect. + * @property {number} targetY The y-coordinate of the target location. + * Moving it instead of just using the `y` parameter will trigger the drift effect. * * @property {Copy|false} follow If set, the camera will follow the given copy. - * @property {boolean} followX Works if `follow` is set to a copy. Enables following in X axis. Set it to `false` and followY to `true` to limit automatic camera movement to vertical axis. - * @property {boolean} followY Works if `follow` is set to a copy. Enables following in Y axis. Set it to `false` and followX to `true` to limit automatic camera movement to horizontal axis. - * @property {number|null} borderX Works if `follow` is set to a copy. Sets the frame inside which the copy will be kept, in game pixels. Can be set to `null` so the copy is set to the center of the screen. - * @property {number|null} borderY Works if `follow` is set to a copy. Sets the frame inside which the copy will be kept, in game pixels. Can be set to `null` so the copy is set to the center of the screen. - * @property {number} shiftX Displaces the camera horizontally but does not change x and y parameters. - * @property {number} shiftY Displaces the camera vertically but does not change x and y parameters. - * @property {number} drift Works if `follow` is set to a copy. If set to a value between 0 and 1, it will make camera movement smoother + * @property {boolean} followX Works if `follow` is set to a copy. + * Enables following in X axis. Set it to `false` and followY to `true` + * to limit automatic camera movement to vertical axis. + * @property {boolean} followY Works if `follow` is set to a copy. + * Enables following in Y axis. Set it to `false` and followX to `true` + * to limit automatic camera movement to horizontal axis. + * @property {number|null} borderX Works if `follow` is set to a copy. + * Sets the frame inside which the copy will be kept, in game pixels. + * Can be set to `null` so the copy is set to the center of the screen. + * @property {number|null} borderY Works if `follow` is set to a copy. + * Sets the frame inside which the copy will be kept, in game pixels. + * Can be set to `null` so the copy is set to the center of the screen. + * @property {number} shiftX Displaces the camera horizontally + * but does not change x and y parameters. + * @property {number} shiftY Displaces the camera vertically + * but does not change x and y parameters. + * @property {number} drift Works if `follow` is set to a copy. + * If set to a value between 0 and 1, it will make camera movement smoother * - * @property {number} shake The current power of a screen shake effect, relative to the screen's max side (100 is 100% of screen shake). If set to 0 or less, it, disables the effect. + * @property {number} shake The current power of a screen shake effect, + * relative to the screen's max side (100 is 100% of screen shake). + * If set to 0 or less, it, disables the effect. * @property {number} shakePhase The current phase of screen shake oscillation. * @property {number} shakeDecay The amount of `shake` units substracted in a second. Default is 5. * @property {number} shakeFrequency The base frequency of the screen shake effect. Default is 50. - * @property {number} shakeX A multiplier applied to the horizontal screen shake effect. Default is 1. - * @property {number} shakeY A multiplier applied to the vertical screen shake effect. Default is 1. - * @property {number} shakeMax The maximum possible value for the `shake` property to protect players from losing their monitor, in `shake` units. Default is 10. + * @property {number} shakeX A multiplier applied to the horizontal screen shake effect. + * Default is 1. + * @property {number} shakeY A multiplier applied to the vertical screen shake effect. + * Default is 1. + * @property {number} shakeMax The maximum possible value for the `shake` property + * to protect players from losing their monitor, in `shake` units. Default is 10. */ class Camera extends PIXI.DisplayObject { constructor(x, y, w, h) { @@ -97,16 +123,21 @@ class Camera extends PIXI.DisplayObject { if (this.shakeMax) { this.shake = Math.min(this.shake, this.shakeMax); } - this.shakePhase += sec * this.shakeFrequency; - this.shakePhaseX += sec * this.shakeFrequency * (1 + Math.sin(this.shakePhase * 0.1489) * 0.25); // no logic in these constants - this.shakePhaseY += sec * this.shakeFrequency * (1 + Math.sin(this.shakePhase * 0.1734) * 0.25); // They are used to desync fluctuations and remove repetitive circular movements + const phaseDelta = sec * this.shakeFrequency; + this.shakePhase += phaseDelta; + // no logic in these constants + // They are used to desync fluctuations and remove repetitive circular movements + this.shakePhaseX += phaseDelta * (1 + Math.sin(this.shakePhase * 0.1489) * 0.25); + this.shakePhaseY += phaseDelta * (1 + Math.sin(this.shakePhase * 0.1734) * 0.25); // The speed of drift movement - const speed = this.drift? Math.min(1, (1-this.drift)*delta) : 1; + const speed = this.drift ? Math.min(1, (1 - this.drift) * delta) : 1; if (this.follow && ('x' in this.follow) && ('y' in this.follow)) { - const bx = this.borderX === null? this.width / 2 : Math.min(this.borderX, this.width / 2), - by = this.borderY === null? this.height / 2 : Math.min(this.borderY, this.height / 2); + // eslint-disable-next-line max-len + const bx = this.borderX === null ? this.width / 2 : Math.min(this.borderX, this.width / 2), + // eslint-disable-next-line max-len + by = this.borderY === null ? this.height / 2 : Math.min(this.borderY, this.height / 2); const tl = this.uiToGameCoord(bx, by), br = this.uiToGameCoord(this.width - bx, this.height - by); @@ -126,10 +157,10 @@ class Camera extends PIXI.DisplayObject { } } - this.x = this.targetX * speed + this.x * (1-speed); - this.y = this.targetY * speed + this.y * (1-speed); - this.interpolatedShiftX = this.shiftX * speed + this.interpolatedShiftX * (1-speed); - this.interpolatedShiftY = this.shiftY * speed + this.interpolatedShiftY * (1-speed); + this.x = this.targetX * speed + this.x * (1 - speed); + this.y = this.targetY * speed + this.y * (1 - speed); + this.interpolatedShiftX = this.shiftX * speed + this.interpolatedShiftX * (1 - speed); + this.interpolatedShiftY = this.shiftY * speed + this.interpolatedShiftY * (1 - speed); this.x = this.x || 0; this.y = this.y || 0; @@ -140,7 +171,7 @@ class Camera extends PIXI.DisplayObject { * @type {number} */ get computedX() { - const dx = (Math.sin(this.shakePhaseX) + Math.sin(this.shakePhaseX * 3.1846)*0.25) / 1.25; + const dx = (Math.sin(this.shakePhaseX) + Math.sin(this.shakePhaseX * 3.1846) * 0.25) / 1.25; const x = this.x + dx * this.shake * Math.max(this.width, this.height) / 100 * this.shakeX; return x + this.interpolatedShiftX; } @@ -149,15 +180,17 @@ class Camera extends PIXI.DisplayObject { * @type {number} */ get computedY() { - const dy = (Math.sin(this.shakePhaseY) + Math.sin(this.shakePhaseY * 2.8948)*0.25) / 1.25; + const dy = (Math.sin(this.shakePhaseY) + Math.sin(this.shakePhaseY * 2.8948) * 0.25) / 1.25; const y = this.y + dy * this.shake * Math.max(this.width, this.height) / 100 * this.shakeY; return y + this.interpolatedShiftY; } /** * Returns the position of the left edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getTopLeftCorner` and `getBottomLeftCorner` methods. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getTopLeftCorner` + * and `getBottomLeftCorner` methods. * @returns {number} The location of the left edge. * @type {number} * @readonly @@ -167,7 +200,8 @@ class Camera extends PIXI.DisplayObject { } /** * Returns the position of the top edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. This does not count for rotations, though. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. * For rotated and/or scaled viewports, see `getTopLeftCorner` and `getTopRightCorner` methods. * @returns {number} The location of the top edge. * @type {number} @@ -178,8 +212,10 @@ class Camera extends PIXI.DisplayObject { } /** * Returns the position of the right edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getTopRightCorner` and `getBottomRightCorner` methods. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getTopRightCorner` + * and `getBottomRightCorner` methods. * @returns {number} The location of the right edge. * @type {number} * @readonly @@ -188,9 +224,11 @@ class Camera extends PIXI.DisplayObject { return this.computedX + (this.width / 2) * this.scale.x; } /** - * Returns the position of the bottom edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getBottomLeftCorner` and `getBottomRightCorner` methods. + * Returns the position of the bottom edge where the visible rectangle ends, + * in game coordinates. This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getBottomLeftCorner` + * and `getBottomRightCorner` methods. * @returns {number} The location of the bottom edge. * @type {number} * @readonly @@ -229,7 +267,8 @@ class Camera extends PIXI.DisplayObject { } /** * Gets the position of the top-left corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, especially with rotated viewports. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. * @returns {Array} A pair of `x` and `y` coordinates. */ getTopLeftCorner() { @@ -238,7 +277,8 @@ class Camera extends PIXI.DisplayObject { /** * Gets the position of the top-right corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, especially with rotated viewports. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. * @returns {Array} A pair of `x` and `y` coordinates. */ getTopRightCorner() { @@ -247,7 +287,8 @@ class Camera extends PIXI.DisplayObject { /** * Gets the position of the bottom-left corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, especially with rotated viewports. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. * @returns {Array} A pair of `x` and `y` coordinates. */ getBottomLeftCorner() { @@ -256,7 +297,8 @@ class Camera extends PIXI.DisplayObject { /** * Gets the position of the bottom-right corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, especially with rotated viewports. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. * @returns {Array} A pair of `x` and `y` coordinates. */ getBottomRightCorner() { @@ -298,7 +340,8 @@ class Camera extends PIXI.DisplayObject { * Realigns all the copies in a room so that they distribute proportionally * to a new camera size based on their `xstart` and `ystart` coordinates. * Will throw an error if the given room is not in UI space (if `room.isUi` is not `true`). - * You can skip the realignment for some copies if you set their `skipRealign` parameter to `true`. + * You can skip the realignment for some copies + * if you set their `skipRealign` parameter to `true`. * @param {Room} room The room which copies will be realigned. * @returns {void} */ @@ -324,8 +367,8 @@ class Camera extends PIXI.DisplayObject { manageStage() { const px = this.computedX, py = this.computedY, - sx = 1 / (isNaN(this.scale.x)? 1 : this.scale.x), - sy = 1 / (isNaN(this.scale.y)? 1 : this.scale.y); + sx = 1 / (isNaN(this.scale.x) ? 1 : this.scale.x), + sy = 1 / (isNaN(this.scale.y) ? 1 : this.scale.y); for (const item of ct.stage.children) { if (!item.isUi && item.pivot) { item.x = -this.width / 2; diff --git a/app/data/ct.release/emitters.js b/app/data/ct.release/emitters.js index 90e3a6e0c..2d230d369 100644 --- a/app/data/ct.release/emitters.js +++ b/app/data/ct.release/emitters.js @@ -9,19 +9,20 @@ * * @property {ISimplePoint} [scale] Optional scaling object with `x` and `y` parameters. * @property {ISimplePoint} [position] Set this to additionally shift the emitter tandem relative - * to the copy it was attached to, or relative to the copy it follows. + * to the copy it was attached to, or relative to the copy it follows. * @property {number} [prewarmDelay] Optional; if less than 0, it will prewarm the emitter tandem, - * meaning that it will simulate a given number of seconds before - * showing particles in the world. If greater than 0, will postpone - * the effect for the specified number of seconds. + * meaning that it will simulate a given number of seconds before + * showing particles in the world. If greater than 0, will postpone + * the effect for the specified number of seconds. * @property {number} [tint] Optional tint to the whole effect. * @property {number} [alpha] Optional opacity set to the whole effect. * @property {number} [rotation] Optional rotation in degrees. * @property {boolean} [isUi] If set to true, will use the time scale of UI layers. This affects - * how an effect is simulated during slowmo effects and game pause. - * @property {number} [depth] The depth of the tandem. Defaults to Infinity (will overlay everything). - * @property {Room} [room] The room to attach the effect to. Defaults to the current main room (ct.room); - * has no effect if attached to a copy. + * how an effect is simulated during slowmo effects and game pause. + * @property {number} [depth] The depth of the tandem. Defaults to Infinity + * (will overlay everything). + * @property {Room} [room] The room to attach the effect to. + * Defaults to the current main room (ct.room); has no effect if attached to a copy. */ /** @@ -33,7 +34,8 @@ */ class EmitterTandem extends PIXI.Container { /** - * Creates a new emitter tandem. This method should not be called directly; better use the methods of `ct.emitters`. + * Creates a new emitter tandem. This method should not be called directly; + * better use the methods of `ct.emitters`. * @param {object} tandemData The template object of the tandem, as it was exported from ct.IDE. * @param {ITandemSettings} opts Additional settings applied to the tandem * @constructor @@ -103,7 +105,7 @@ class EmitterTandem extends PIXI.Container { } } // eslint-disable-next-line no-underscore-dangle - if ((this.appendant && this.appendant._destroyed) || this.kill || !this.emitters.length) { + if ((this.appendant && this.appendant._destroyed) || this.kill || !this.emitters.length) { this.emit('done'); if (this.isUi) { ct.emitters.uiTandems.splice(ct.emitters.uiTandems.indexOf(this), 1); @@ -116,7 +118,7 @@ class EmitterTandem extends PIXI.Container { if (this.frozen) { return; } - const s = (this.isUi? PIXI.Ticker.shared.elapsedMS : PIXI.Ticker.shared.deltaMS) / 1000; + const s = (this.isUi ? PIXI.Ticker.shared.elapsedMS : PIXI.Ticker.shared.deltaMS) / 1000; for (const delayed of this.delayed) { delayed.value -= s; if (delayed.value <= 0) { @@ -205,10 +207,9 @@ class EmitterTandem extends PIXI.Container { emitter.updateSpawnPos(ownDelta[0], ownDelta[1]); } } - } -(function () { +(function emittersAddon() { const defaultSettings = { prewarmDelay: 0, scale: { @@ -270,7 +271,8 @@ class EmitterTandem extends PIXI.Container { return tandem; }, /** - * Creates a new emitter tandem and attaches it to the given copy (or to any other DisplayObject). + * Creates a new emitter tandem and attaches it to the given copy + * (or to any other DisplayObject). * @param {Copy|PIXI.DisplayObject} parent The parent of the created tandem. * @param {string} name The name of the tandem template. * @param {ITandemSettings} [settings] Additional options for the created tandem. diff --git a/app/data/ct.release/inputs.js b/app/data/ct.release/inputs.js index 6ca0b713e..e5f6980b6 100644 --- a/app/data/ct.release/inputs.js +++ b/app/data/ct.release/inputs.js @@ -6,8 +6,8 @@ class CtAction { /** * This is a custom action defined in the Settings tab → Edit actions section. * Actions are used to abstract different input methods into one gameplay-related interface: - * for example, joystick movement, WASD keys and arrows can be turned into two actions: `MoveHorizontally` - * and `MoveVertically`. + * for example, joystick movement, WASD keys and arrows can be turned into two actions: + * `MoveHorizontally` and `MoveVertically`. * @param {string} name The name of the new action. */ constructor(name) { @@ -32,13 +32,14 @@ class CtAction { * Adds a new input method to listen. * * @param {string} code The input method's code to listen to. Must be unique per action. - * @param {number} [multiplier] An optional multiplier, e.g. to flip its value. Often used with two buttons to combine them into a scalar input identical to joysticks + * @param {number} [multiplier] An optional multiplier, e.g. to flip its value. + * Often used with two buttons to combine them into a scalar input identical to joysticks. * @returns {void} */ addMethod(code, multiplier) { if (this.methodCodes.indexOf(code) === -1) { this.methodCodes.push(code); - this.methodMultipliers.push(multiplier !== void 0? multiplier : 1); + this.methodMultipliers.push(multiplier !== void 0 ? multiplier : 1); } else { throw new Error(`[ct.inputs] An attempt to add an already added input "${code}" to an action "${name}".`); } @@ -83,7 +84,8 @@ class CtAction { this.prevValue = this.value; this.value = 0; for (let i = 0, l = this.methodCodes.length; i < l; i++) { - this.value += (ct.inputs.registry[this.methodCodes[i]] || 0) * this.methodMultipliers[i]; + const rawValue = ct.inputs.registry[this.methodCodes[i]] || 0; + this.value += rawValue * this.methodMultipliers[i]; } this.value = Math.max(-1, Math.min(this.value, 1)); } diff --git a/app/data/ct.release/main.js b/app/data/ct.release/main.js index 5c1ac2a4f..964b72f66 100644 --- a/app/data/ct.release/main.js +++ b/app/data/ct.release/main.js @@ -1,10 +1,8 @@ /* Made with ct.js http://ctjs.rocks/ */ -/* global CtTimer */ - const deadPool = []; // a pool of `kill`-ed copies for delaying frequent garbage collection const copyTypeSymbol = Symbol('I am a ct.js copy'); -setInterval(function () { +setInterval(function cleanDeadPool() { deadPool.length = 0; }, 1000 * 60); @@ -30,7 +28,8 @@ const ct = { * Use ct.delta to balance your movement and other calculations on different framerates by * multiplying it with your reference value. * - * Note that `this.move()` already uses it, so there is no need to premultiply `this.speed` with it. + * Note that `this.move()` already uses it, so there is no need to premultiply + * `this.speed` with it. * * **A minimal example:** * ```js @@ -128,7 +127,7 @@ console.log( 'background: #446adb; color: #fff; padding: 0.5em 0;', 'background: #5144db; color: #fff; padding: 0.5em 0;', 'background: #446adb; color: #fff; padding: 0.5em 0;', - 'background: #5144db; color: #fff; padding: 0.5em 0;', + 'background: #5144db; color: #fff; padding: 0.5em 0;' ); ct.highDensity = [/*@highDensity@*/][0]; @@ -299,7 +298,8 @@ ct.u = { * @param {number} a The first value to interpolate from * @param {number} b The second value to interpolate top * @param {number} val The interpolated values - * @return {number} The position of the value in the specified range. When a <= val <= b, the result will be inside the [0;1] range. + * @return {number} The position of the value in the specified range. + * When a <= val <= b, the result will be inside the [0;1] range. */ unlerp(a, b, val) { return (val - a) / (b - a); @@ -323,11 +323,14 @@ ct.u = { return ct.camera.gameToUiCoord(x, y); }, /** - * Tests whether a given point is inside the given rectangle (it can be either a copy or an array) - * @param {number} x The x coordinate of the point - * @param {number} y The y coordinate of the point - * @param {(Copy|Array)} arg Either a copy (it must have a rectangular shape) or an array in a form of [x1, y1, x2, y2], where (x1;y1) and (x2;y2) specify the two opposite corners of the rectangle - * @returns {boolean} `true` if the point is inside the rectangle, `false` otherwise + * Tests whether a given point is inside the given rectangle + * (it can be either a copy or an array). + * @param {number} x The x coordinate of the point. + * @param {number} y The y coordinate of the point. + * @param {(Copy|Array)} arg Either a copy (it must have a rectangular shape) + * or an array in a form of [x1, y1, x2, y2], where (x1;y1) and (x2;y2) specify + * the two opposite corners of the rectangle. + * @returns {boolean} `true` if the point is inside the rectangle, `false` otherwise. */ prect(x, y, arg) { var xmin, xmax, ymin, ymax; @@ -348,7 +351,9 @@ ct.u = { * Tests whether a given point is inside the given circle (it can be either a copy or an array) * @param {number} x The x coordinate of the point * @param {number} y The y coordinate of the point - * @param {(Copy|Array)} arg Either a copy (it must have a circular shape) or an array in a form of [x1, y1, r], where (x1;y1) define the center of the circle and `r` defines the radius of it + * @param {(Copy|Array)} arg Either a copy (it must have a circular shape) + * or an array in a form of [x1, y1, r], where (x1;y1) define the center of the circle + * and `r` defines the radius of it. * @returns {boolean} `true` if the point is inside the circle, `false` otherwise */ pcircle(x, y, arg) { @@ -358,10 +363,13 @@ ct.u = { return ct.u.pdc(0, 0, (arg.x - x) / arg.scale.x, (arg.y - y) / arg.scale.y) < arg.shape.r; }, /** - * Copies all the properties of the source object to the destination object. This is **not** a deep copy. Useful for extending some settings with default values, or for combining data. + * Copies all the properties of the source object to the destination object. + * This is **not** a deep copy. Useful for extending some settings with default values, + * or for combining data. * @param {object} o1 The destination object * @param {object} o2 The source object - * @param {any} [arr] An optional array of properties to copy. If not specified, all the properties will be copied. + * @param {any} [arr] An optional array of properties to copy. If not specified, + * all the properties will be copied. * @returns {object} The modified destination object */ ext(o1, o2, arr) { @@ -380,7 +388,8 @@ ct.u = { }, /** * Loads and executes a script by its URL, optionally with a callback - * @param {string} url The URL of the script file, with its extension. Can be relative or absolute + * @param {string} url The URL of the script file, with its extension. + * Can be relative or absolute. * @param {Function} callback An optional callback that fires when the script is loaded * @returns {void} */ @@ -421,6 +430,7 @@ ct.u.ext(ct.u, {// make aliases extend: ct.u.ext }); +// eslint-disable-next-line max-lines-per-function (() => { const killRecursive = copy => { copy.kill = true; @@ -452,7 +462,7 @@ ct.u.ext(ct.u, {// make aliases } }; - ct.loop = function (delta) { + ct.loop = function loop(delta) { ct.delta = delta; ct.deltaUi = PIXI.Ticker.shared.elapsedMS / (1000 / (PIXI.Ticker.shared.maxFPS || 60)); ct.inputs.updateActions(); @@ -476,15 +486,17 @@ ct.u.ext(ct.u, {// make aliases for (const copy of ct.stack) { // eslint-disable-next-line no-underscore-dangle if (copy.kill && !copy._destroyed) { - killRecursive(copy); // This will also allow a parent to eject children to a new container before they are destroyed as well - copy.destroy({children: true}); + killRecursive(copy); // This will also allow a parent to eject children + // to a new container before they are destroyed as well + copy.destroy({ + children: true + }); } } for (const cont of ct.stage.children) { cont.children.sort((a, b) => - ((a.depth || 0) - (b.depth || 0)) || ((a.uid || 0) - (b.uid || 0)) || 0 - ); + ((a.depth || 0) - (b.depth || 0)) || ((a.uid || 0) - (b.uid || 0)) || 0); } manageCamera(); diff --git a/app/data/ct.release/res.js b/app/data/ct.release/res.js index b94f3934c..b5fa23763 100644 --- a/app/data/ct.release/res.js +++ b/app/data/ct.release/res.js @@ -1,9 +1,8 @@ -(function (ct) { +(function resAddon(ct) { const loader = new PIXI.Loader(); const loadingScreen = document.querySelector('.ct-aLoadingScreen'), loadingBar = loadingScreen.querySelector('.ct-aLoadingBar'); - /* global dragonBones */ - const dbFactory = window.dragonBones? dragonBones.PixiFactory.factory : null; + const dbFactory = window.dragonBones ? dragonBones.PixiFactory.factory : null; /** * An utility object that managess and stores textures and other entities * @namespace @@ -34,12 +33,16 @@ PIXI.Loader.shared.load(); }, /* - * Gets a pixi.js texture from a ct.js' texture name, so that it can be used in pixi.js objects. + * Gets a pixi.js texture from a ct.js' texture name, + so that it can be used in pixi.js objects. * @param {string} name The name of the ct.js texture * @param {number} [frame] The frame to extract - * @returns {PIXI.Texture|Array} If `frame` was specified, returns a single PIXI.Texture. Otherwise, returns an array with all the frames of this ct.js' texture. + * @returns {PIXI.Texture|Array} If `frame` was specified, + * returns a single PIXI.Texture. Otherwise, returns an array + * with all the frames of this ct.js' texture. * - * @note Formatted as a non-jsdoc comment as it requires a better ts declaration than the auto-generated one + * @note Formatted as a non-jsdoc comment as it requires a better ts declaration + * than the auto-generated one */ getTexture(name, frame) { if (name === -1) { @@ -64,7 +67,7 @@ const r = ct.res.skelRegistry[name], skel = dbFactory.buildArmatureDisplay('Armature', r.data.name, skin); skel.ctName = name; - skel.on(dragonBones.EventObject.SOUND_EVENT, function (event) { + skel.on(dragonBones.EventObject.SOUND_EVENT, function skeletonSound(event) { if (ct.sound.exists(event.name)) { ct.sound.spawn(event.name); } else { @@ -88,8 +91,7 @@ for (let i = 0; i < reg.frames; i++) { const frame = `${texture}@frame${i}`; const atlas = PIXI.Loader.shared.resources[ct.res.atlases.find(atlas => - frame in PIXI.Loader.shared.resources[atlas].textures - )]; + frame in PIXI.Loader.shared.resources[atlas].textures)]; const tex = atlas.textures[frame]; tex.defaultAnchor = new PIXI.Point(reg.anchor.x, reg.anchor.y); reg.textures.push(tex); @@ -101,6 +103,7 @@ } } for (const skel in ct.res.skelRegistry) { + // eslint-disable-next-line id-blacklist ct.res.skelRegistry[skel].data = PIXI.Loader.shared.resources[ct.res.skelRegistry[skel].origname + '_ske.json'].data; } /*%resload%*/ diff --git a/app/data/ct.release/rooms.js b/app/data/ct.release/rooms.js index ff1d913e0..307271dd6 100644 --- a/app/data/ct.release/rooms.js +++ b/app/data/ct.release/rooms.js @@ -24,7 +24,12 @@ class Room extends PIXI.Container { this.template = template; this.name = template.name; for (let i = 0, li = template.bgs.length; i < li; i++) { - const bg = new ct.types.Background(template.bgs[i].texture, null, template.bgs[i].depth, template.bgs[i].extends); + const bg = new ct.types.Background( + template.bgs[i].texture, + null, + template.bgs[i].depth, + template.bgs[i].extends + ); this.backgrounds.push(bg); ct.stack.push(bg); this.addChild(bg); @@ -35,31 +40,37 @@ class Room extends PIXI.Container { this.addChild(tl); } for (let i = 0, li = template.objects.length; i < li; i++) { - ct.types.make(template.objects[i].type, template.objects[i].x, template.objects[i].y, { - tx: template.objects[i].tx, - ty: template.objects[i].ty, - tr: template.objects[i].tr, - }, this); + ct.types.make( + template.objects[i].type, + template.objects[i].x, + template.objects[i].y, + { + tx: template.objects[i].tx, + ty: template.objects[i].ty, + tr: template.objects[i].tr + }, + this + ); } } return this; } - get x () { + get x() { return -this.position.x; } - set x (value) { + set x(value) { this.position.x = -value; return value; } - get y () { + get y() { return -this.position.y; } - set y (value) { + set y(value) { this.position.y = -value; return value; } } -(function () { +(function roomsAddon() { /* global deadPool */ var nextRoom; /** @@ -111,10 +122,12 @@ class Room extends PIXI.Container { }, /** * This method safely removes a previously appended/prepended room from the stage. - * It will trigger "On Leave" for a room and "On Destroy" event for all the copies of the removed room. + * It will trigger "On Leave" for a room and "On Destroy" event + * for all the copies of the removed room. * The room will also have `this.kill` set to `true` in its event, if it comes in handy. * This method cannot remove `ct.room`, the main room. - * @param {Room} room The `room` argument must be a reference to the previously created room. + * @param {Room} room The `room` argument must be a reference + * to the previously created room. * @returns {void} */ remove(room) { @@ -153,10 +166,12 @@ class Room extends PIXI.Container { }, switching: false, /** - * Creates a new room and adds it to the stage, separating its draw stack from existing ones. + * Creates a new room and adds it to the stage, separating its draw stack + * from existing ones. * This room is added to `ct.stage` after all the other rooms. * @param {string} roomName The name of the room to be appended - * @param {object} [exts] Any additional parameters applied to the new room. Useful for passing settings and data to new widgets and prefabs. + * @param {object} [exts] Any additional parameters applied to the new room. + * Useful for passing settings and data to new widgets and prefabs. * @returns {Room} A newly created room */ append(roomName, exts) { @@ -175,10 +190,12 @@ class Room extends PIXI.Container { return room; }, /** - * Creates a new room and adds it to the stage, separating its draw stack from existing ones. + * Creates a new room and adds it to the stage, separating its draw stack + * from existing ones. * This room is added to `ct.stage` before all the other rooms. * @param {string} roomName The name of the room to be prepended - * @param {object} [exts] Any additional parameters applied to the new room. Useful for passing settings and data to new widgets and prefabs. + * @param {object} [exts] Any additional parameters applied to the new room. + * Useful for passing settings and data to new widgets and prefabs. * @returns {Room} A newly created room */ prepend(roomName, exts) { @@ -201,8 +218,8 @@ class Room extends PIXI.Container { * * @param {string} roomName The name of the room that needs to be merged * @returns {IRoomMergeResult} Arrays of created copies, backgrounds, tile layers, - * added to the current room (`ct.room`). Note: it does not get updated, so beware of memory leaks - * if you keep a reference to this array for a long time! + * added to the current room (`ct.room`). Note: it does not get updated, + * so beware of memory leaks if you keep a reference to this array for a long time! */ merge(roomName) { if (!(roomName in ct.rooms.templates)) { @@ -253,7 +270,12 @@ class Room extends PIXI.Container { var template = ct.rooms.templates[roomName]; ct.roomWidth = template.width; ct.roomHeight = template.height; - ct.camera = new Camera(ct.roomWidth / 2, ct.roomHeight / 2, ct.roomWidth, ct.roomHeight); + ct.camera = new Camera( + ct.roomWidth / 2, + ct.roomHeight / 2, + ct.roomWidth, + ct.roomHeight + ); ct.pixiApp.renderer.resize(template.width, template.height); /*%beforeroomoncreate%*/ ct.rooms.current = ct.room = new Room(template); @@ -285,16 +307,16 @@ class Room extends PIXI.Container { */ ct.room = null; -ct.rooms.beforeStep = function () { +ct.rooms.beforeStep = function beforeStep() { /*%beforeroomstep%*/ }; -ct.rooms.afterStep = function () { +ct.rooms.afterStep = function afterStep() { /*%afterroomstep%*/ }; -ct.rooms.beforeDraw = function () { +ct.rooms.beforeDraw = function beforeDraw() { /*%beforeroomdraw%*/ }; -ct.rooms.afterDraw = function () { +ct.rooms.afterDraw = function afterDraw() { /*%afterroomdraw%*/ }; diff --git a/app/data/ct.release/sound.js b/app/data/ct.release/sound.js index 9764fa27c..971cac693 100644 --- a/app/data/ct.release/sound.js +++ b/app/data/ct.release/sound.js @@ -1,108 +1,109 @@ -if (!ct.sound) { - /** - * @namespace - */ - ct.sound = { - /** - * Detects if a particular codec is supported in the system - * @param {string} type Codec/MIME-type to look for - * @returns {boolean} true/false - */ - detect(type) { - var au = document.createElement('audio'); - return Boolean(au.canPlayType && au.canPlayType(type).replace(/no/, '')); - }, - /** - * Creates a new Sound object and puts it in resource object - * - * @param {string} name Sound's name - * @param {object} formats A collection of sound files of specified extension, in format `extension: path` - * @param {string} [formats.ogg] Local path to the sound in ogg format - * @param {string} [formats.wav] Local path to the sound in wav format - * @param {string} [formats.mp3] Local path to the sound in mp3 format - * @param {number} [options] An options object - * - * @returns {object} Sound's object - */ - init(name, formats, options) { - var src = ''; - if (ct.sound.mp3 && formats.mp3) { - src = formats.mp3; - } else if (ct.sound.ogg && formats.ogg) { - src = formats.ogg; - } else if (ct.sound.wav && formats.wav) { - src = formats.wav; - } - options = options || {}; - var audio = { - src, - direct: document.createElement('audio'), - pool: [], - poolSize: options.poolSize || 5 - }; - if (src !== '') { - ct.res.soundsLoaded++; - audio.direct.preload = options.music? 'metadata' : 'auto'; - audio.direct.onerror = audio.direct.onabort = function () { - console.error('[ct.sound] Oh no! We couldn\'t load ' + src + '!'); - audio.buggy = true; - ct.res.soundsError++; - ct.res.soundsLoaded--; - }; - audio.direct.src = src; - } else { - ct.res.soundsError++; - audio.buggy = true; - console.error('[ct.sound] We couldn\'t load sound named "' + name + '" because the browser doesn\'t support any of proposed formats.'); - } - ct.res.sounds[name] = audio; - return audio; - }, - /** - * Spawns a new sound and plays it. - * - * @param {string} name The name of sound to be played - * @param {object} [opts] Options object that is applied to a newly created audio tag - * @param {Function} [cb] A callback, which is called when the sound finishes playing - * - * @returns {HTMLTagAudio|Boolean} The created audio or `false` (if a sound wasn't created) - */ - spawn(name, opts, cb) { - opts = opts || {}; - if (typeof opts === 'function') { - cb = opts; - } - var s = ct.res.sounds[name]; - if (s.pool.length < s.poolSize) { - var a = document.createElement('audio'); - a.src = s.src; - if (opts) { - ct.u.ext(a, opts); - } - s.pool.push(a); - a.addEventListener('ended', function (e) { - s.pool.splice(s.pool.indexOf(a), 1); - if (cb) { - cb(true, e); - } - }); - - a.play(); - return a; - } else if (cb) { - cb(false); - } - return false; - }, - exists(name) { - return (name in ct.res.sounds); - } - }; - - // define sound types we can support - ct.sound.wav = ct.sound.detect('audio/wav; codecs="1"'); - ct.sound.mp3 = ct.sound.detect('audio/mpeg;'); - ct.sound.ogg = ct.sound.detect('audio/ogg;'); -} - -/*@sound@*/ +if (!ct.sound) { + /** + * @namespace + */ + ct.sound = { + /** + * Detects if a particular codec is supported in the system + * @param {string} type Codec/MIME-type to look for + * @returns {boolean} true/false + */ + detect(type) { + var au = document.createElement('audio'); + return Boolean(au.canPlayType && au.canPlayType(type).replace(/no/, '')); + }, + /** + * Creates a new Sound object and puts it in resource object + * + * @param {string} name Sound's name + * @param {object} formats A collection of sound files of specified extension, + * in format `extension: path` + * @param {string} [formats.ogg] Local path to the sound in ogg format + * @param {string} [formats.wav] Local path to the sound in wav format + * @param {string} [formats.mp3] Local path to the sound in mp3 format + * @param {number} [options] An options object + * + * @returns {object} Sound's object + */ + init(name, formats, options) { + var src = ''; + if (ct.sound.mp3 && formats.mp3) { + src = formats.mp3; + } else if (ct.sound.ogg && formats.ogg) { + src = formats.ogg; + } else if (ct.sound.wav && formats.wav) { + src = formats.wav; + } + options = options || {}; + var audio = { + src, + direct: document.createElement('audio'), + pool: [], + poolSize: options.poolSize || 5 + }; + if (src !== '') { + ct.res.soundsLoaded++; + audio.direct.preload = options.music ? 'metadata' : 'auto'; + audio.direct.onerror = audio.direct.onabort = function onabort() { + console.error('[ct.sound] Oh no! We couldn\'t load ' + src + '!'); + audio.buggy = true; + ct.res.soundsError++; + ct.res.soundsLoaded--; + }; + audio.direct.src = src; + } else { + ct.res.soundsError++; + audio.buggy = true; + console.error('[ct.sound] We couldn\'t load sound named "' + name + '" because the browser doesn\'t support any of proposed formats.'); + } + ct.res.sounds[name] = audio; + return audio; + }, + /** + * Spawns a new sound and plays it. + * + * @param {string} name The name of sound to be played + * @param {object} [opts] Options object that is applied to a newly created audio tag + * @param {Function} [cb] A callback, which is called when the sound finishes playing + * + * @returns {HTMLTagAudio|Boolean} The created audio or `false` (if a sound wasn't created) + */ + spawn(name, opts, cb) { + opts = opts || {}; + if (typeof opts === 'function') { + cb = opts; + } + var s = ct.res.sounds[name]; + if (s.pool.length < s.poolSize) { + var a = document.createElement('audio'); + a.src = s.src; + if (opts) { + ct.u.ext(a, opts); + } + s.pool.push(a); + a.addEventListener('ended', function soundEnded(e) { + s.pool.splice(s.pool.indexOf(a), 1); + if (cb) { + cb(true, e); + } + }); + + a.play(); + return a; + } else if (cb) { + cb(false); + } + return false; + }, + exists(name) { + return (name in ct.res.sounds); + } + }; + + // define sound types we can support + ct.sound.wav = ct.sound.detect('audio/wav; codecs="1"'); + ct.sound.mp3 = ct.sound.detect('audio/mpeg;'); + ct.sound.ogg = ct.sound.detect('audio/ogg;'); +} + +/*@sound@*/ diff --git a/app/data/ct.release/styles.js b/app/data/ct.release/styles.js index 88a04a49f..20b7200bc 100644 --- a/app/data/ct.release/styles.js +++ b/app/data/ct.release/styles.js @@ -1,32 +1,36 @@ -/** - * @namespace - */ -ct.styles = { - types: { }, - /* - * Creates a new style with a given name. Technically, it just writes `data` to `ct.styles.types` - */ - new(name, data) { - ct.styles.types[name] = data; - return data; - }, - /** - * Returns a style of a given name. The actual behavior strongly depends on `copy` parameter. - * @param {string} name The name of the style to load - * @param {boolean|Object} [copy] If not set, returns the source style object. Editing it will affect all new style calls. - * When set to `true`, will create a new object, which you can safely modify without affecting the source style. - * When set to an object, this will create a new object as well, augmenting it with given properties. - * @returns {object} The resulting style - */ - get(name, copy) { - if (copy === true) { - return ct.u.ext({}, ct.styles.types[name]); - } - if (copy) { - return ct.u.ext(ct.u.ext({}, ct.styles.types[name]), copy); - } - return ct.styles.types[name]; - } -}; -/*@styles@*/ -/*%styles%*/ +/** + * @namespace + */ +ct.styles = { + types: { }, + /** + * Creates a new style with a given name. + * Technically, it just writes `data` to `ct.styles.types` + */ + new(name, styleTemplate) { + ct.styles.types[name] = styleTemplate; + return styleTemplate; + }, + /** + * Returns a style of a given name. The actual behavior strongly depends on `copy` parameter. + * @param {string} name The name of the style to load + * @param {boolean|Object} [copy] If not set, returns the source style object. + * Editing it will affect all new style calls. + * When set to `true`, will create a new object, which you can safely modify + * without affecting the source style. + * When set to an object, this will create a new object as well, + * augmenting it with given properties. + * @returns {object} The resulting style + */ + get(name, copy) { + if (copy === true) { + return ct.u.ext({}, ct.styles.types[name]); + } + if (copy) { + return ct.u.ext(ct.u.ext({}, ct.styles.types[name]), copy); + } + return ct.styles.types[name]; + } +}; +/*@styles@*/ +/*%styles%*/ diff --git a/app/data/ct.release/timer.js b/app/data/ct.release/timer.js index 37452233e..0e45461f0 100644 --- a/app/data/ct.release/timer.js +++ b/app/data/ct.release/timer.js @@ -1,4 +1,4 @@ -(function () { +(function timerAddon() { const ctTimerTime = Symbol('time'); const ctTimerRoomName = Symbol('roomName'); const ctTimerTimeLeftOriginal = Symbol('timeLeftOriginal'); @@ -15,7 +15,8 @@ * * @param {number} timeMs The length of the timer, **in milliseconds** * @param {string|false} [name=false] The name of the timer - * @param {boolean} [uiDelta=false] If `true`, it will use `ct.deltaUi` for counting time. if `false`, it will use `ct.delta` for counting time. + * @param {boolean} [uiDelta=false] If `true`, it will use `ct.deltaUi` for counting time. + * if `false`, it will use `ct.delta` for counting time. */ constructor(timeMs, name = false, uiDelta = false) { this[ctTimerRoomName] = ct.room.name || ''; @@ -141,20 +142,22 @@ * Adds a new timer with a given name * * @param {number} timeMs The length of the timer, **in milliseconds** - * @param {string|false} [name=false] The name of the timer, which you use to access it from `ct.timer.timers`. + * @param {string|false} [name=false] The name of the timer, which you use + * to access it from `ct.timer.timers`. * @returns {CtTimer} The timer */ - add(timeMs, name=false) { + add(timeMs, name = false) { return new CtTimer(timeMs, name, false); }, /** * Adds a new timer with a given name that runs in a UI time scale * * @param {number} timeMs The length of the timer, **in milliseconds** - * @param {string|false} [name=false] The name of the timer, which you use to access it from `ct.timer.timers`. + * @param {string|false} [name=false] The name of the timer, which you use + * to access it from `ct.timer.timers`. * @returns {CtTimer} The timer */ - addUi(timeMs, name=false) { + addUi(timeMs, name = false) { return new CtTimer(timeMs, name, true); }, /** diff --git a/app/data/ct.release/types.js b/app/data/ct.release/types.js index 5da56106b..14dc33b7c 100644 --- a/app/data/ct.release/types.js +++ b/app/data/ct.release/types.js @@ -37,14 +37,14 @@ class Background extends PIXI.TilingSprite { const cameraBounds = ct.camera.getBoundingBox(); if (this.repeat !== 'repeat-x' && this.repeat !== 'no-repeat') { this.y = cameraBounds.y; - this.tilePosition.y = -this.y*this.parallaxY + this.shiftY; + this.tilePosition.y = -this.y * this.parallaxY + this.shiftY; this.height = cameraBounds.height; } else { this.y = this.shiftY + cameraBounds.y * (this.parallaxY - 1); } if (this.repeat !== 'repeat-y' && this.repeat !== 'no-repeat') { this.x = cameraBounds.x; - this.tilePosition.x = -this.x*this.parallaxX + this.shiftX; + this.tilePosition.x = -this.x * this.parallaxX + this.shiftX; this.width = cameraBounds.width; } else { this.x = this.shiftX + cameraBounds.x * (this.parallaxX - 1); @@ -64,22 +64,22 @@ class Background extends PIXI.TilingSprite { * @extends {PIXI.Container} */ class Tileset extends PIXI.Container { - constructor(data) { + constructor(template) { super(); - this.depth = data.depth; - this.tiles = data.tiles; + this.depth = template.depth; + this.tiles = template.tiles; ct.types.list.TILELAYER.push(this); - for (let i = 0, l = data.tiles.length; i < l; i++) { - const textures = ct.res.getTexture(data.tiles[i].texture); - const sprite = new PIXI.Sprite(textures[data.tiles[i].frame]); + for (let i = 0, l = template.tiles.length; i < l; i++) { + const textures = ct.res.getTexture(template.tiles[i].texture); + const sprite = new PIXI.Sprite(textures[template.tiles[i].frame]); sprite.anchor.x = sprite.anchor.y = 0; this.addChild(sprite); - sprite.x = data.tiles[i].x; - sprite.y = data.tiles[i].y; + sprite.x = template.tiles[i].x; + sprite.y = template.tiles[i].y; } const bounds = this.getLocalBounds(); const cols = Math.ceil(bounds.width / 1024), - rows = Math.ceil(bounds.height / 1024); + rows = Math.ceil(bounds.height / 1024); if (cols < 2 && rows < 2) { if (this.width > 0 && this.height > 0) { this.cacheAsBitmap = true; @@ -100,10 +100,10 @@ class Tileset extends PIXI.Container { this.cells.push(cell); } } - for (let i = 0, l = data.tiles.length; i < l; i++) { + for (let i = 0, l = template.tiles.length; i < l; i++) { const tile = this.children[0], - x = Math.floor((tile.x - bounds.x) / 1024), - y = Math.floor((tile.y - bounds.y) / 1024); + x = Math.floor((tile.x - bounds.x) / 1024), + y = Math.floor((tile.y - bounds.y) / 1024); this.cells[y * cols + x].addChild(tile); /*if (tile.x - x * 1024 + tile.width > 1024) { this.cells[y*cols + x + 1].addChild(tile); @@ -119,7 +119,8 @@ class Tileset extends PIXI.Container { for (let i = 0, l = this.cells.length; i < l; i++) { if (this.cells[i].children.length === 0) { this.cells.splice(i, 1); - i--; l--; + i--; + l--; continue; } //this.cells[i].mask = mask; @@ -133,11 +134,16 @@ class Tileset extends PIXI.Container { * @class * @property {string} type The name of the type from which the copy was created * @property {IShapeTemplate} shape The collision shape of a copy - * @property {number} depth The relative position of a copy in a drawing stack. Higher values will draw the copy on top of those with lower ones + * @property {number} depth The relative position of a copy in a drawing stack. + * Higher values will draw the copy on top of those with lower ones * @property {number} xprev The horizontal location of a copy in the previous frame * @property {number} yprev The vertical location of a copy in the previous frame - * @property {number} xstart The starting location of a copy, meaning the point where it was created — either by placing it in a room with ct.IDE or by calling `ct.types.copy`. - * @property {number} ystart The starting location of a copy, meaning the point where it was created — either by placing it in a room with ct.IDE or by calling `ct.types.copy`. + * @property {number} xstart The starting location of a copy, + * meaning the point where it was created — either by placing it in a room with ct.IDE + * or by calling `ct.types.copy`. + * @property {number} ystart The starting location of a copy, + * meaning the point where it was created — either by placing it in a room with ct.IDE + * or by calling `ct.types.copy`. * @property {number} hspeed The horizontal speed of a copy * @property {number} vspeed The vertical speed of a copy * @property {number} gravity The acceleration that pulls a copy at each frame @@ -145,7 +151,7 @@ class Tileset extends PIXI.Container { * @property {number} depth The position of a copy in draw calls * @property {boolean} kill If set to `true`, the copy will be destroyed by the end of a frame. */ -const Copy = (function () { +const Copy = (function Copy() { const textureAccessor = Symbol('texture'); class Copy extends PIXI.AnimatedSprite { /** @@ -155,7 +161,8 @@ const Copy = (function () { * @param {number} [y] The y coordinate of a new copy. Defaults to 0. * @param {object} [exts] An optional object with additional properties * that will exist prior to a copy's OnCreate event - * @param {PIXI.DisplayObject|Room} [container] A container to set as copy's parent before its OnCreate event. Defaults to ct.room. + * @param {PIXI.DisplayObject|Room} [container] A container to set as copy's parent + * before its OnCreate event. Defaults to ct.room. * @memberof Copy */ constructor(type, x, y, exts, container) { @@ -272,8 +279,8 @@ const Copy = (function () { */ set direction(value) { var speed = this.speed; - this.hspeed = speed * Math.cos(value*Math.PI/-180); - this.vspeed = speed * Math.sin(value*Math.PI/-180); + this.hspeed = speed * Math.cos(value * Math.PI / -180); + this.vspeed = speed * Math.sin(value * Math.PI / -180); return value; } get rotation() { @@ -295,21 +302,22 @@ const Copy = (function () { */ move() { if (this.gravity) { - this.hspeed += this.gravity * ct.delta * Math.cos(this.gravityDir*Math.PI/-180); - this.vspeed += this.gravity * ct.delta * Math.sin(this.gravityDir*Math.PI/-180); + this.hspeed += this.gravity * ct.delta * Math.cos(this.gravityDir * Math.PI / -180); + this.vspeed += this.gravity * ct.delta * Math.sin(this.gravityDir * Math.PI / -180); } this.x += this.hspeed * ct.delta; this.y += this.vspeed * ct.delta; } /** - * Adds a speed vector to the copy, accelerating it by a given delta speed in a given direction. + * Adds a speed vector to the copy, accelerating it by a given delta speed + * in a given direction. * @param {number} spd Additive speed * @param {number} dir The direction in which to apply additional speed * @returns {void} */ addSpeed(spd, dir) { - this.hspeed += spd * Math.cos(dir*Math.PI/-180); - this.vspeed += spd * Math.sin(dir*Math.PI/-180); + this.hspeed += spd * Math.cos(dir * Math.PI / -180); + this.vspeed += spd * Math.sin(dir * Math.PI / -180); } /** @@ -327,7 +335,7 @@ const Copy = (function () { return Copy; })(); -(function (ct) { +(function ctTypeAddon(ct) { const onCreateModifier = function () { /*%oncreate%*/ }; @@ -359,12 +367,14 @@ const Copy = (function () { * @param {string} type The name of the type to use * @param {number} [x] The x coordinate of a new copy. Defaults to 0. * @param {number} [y] The y coordinate of a new copy. Defaults to 0. - * @param {object} [exts] An optional object which parameters will be applied to the copy prior to its OnCreate event. - * @param {PIXI.Container} [container] The container to which add the copy. Defaults to the current room. + * @param {object} [exts] An optional object which parameters will be applied + * to the copy prior to its OnCreate event. + * @param {PIXI.Container} [container] The container to which add the copy. + * Defaults to the current room. * @returns {Copy} the created copy. * @alias ct.types.copy */ - make(type, x=0, y=0, exts, container) { + make(type, x = 0, y = 0, exts, container) { // An advanced constructor. Returns a Copy if (exts instanceof PIXI.Container) { container = exts; @@ -439,19 +449,19 @@ const Copy = (function () { /*@types@*/ /*%types%*/ - ct.types.beforeStep = function () { + ct.types.beforeStep = function beforeStep() { /*%beforestep%*/ }; - ct.types.afterStep = function () { + ct.types.afterStep = function afterStep() { /*%afterstep%*/ }; - ct.types.beforeDraw = function () { + ct.types.beforeDraw = function beforeDraw() { /*%beforedraw%*/ }; - ct.types.afterDraw = function () { + ct.types.afterDraw = function afterDraw() { /*%afterdraw%*/ }; - ct.types.onDestroy = function () { + ct.types.onDestroy = function onDestroy() { /*%ondestroy%*/ }; })(ct); diff --git a/gulpfile.js b/gulpfile.js index e554b73e3..bda975fc3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -213,6 +213,8 @@ const lintJS = () => { './src/js/**/*.js', '!./src/js/3rdparty/**/*.js', './src/node_requires/**/*.js', + './app/data/ct.release/**/*.js', + '!./app/data/ct.release/**/*.min.js', './src/pug/**/*.pug' ]) .pipe(eslint({ From d1fec7c66a5b8ec23045c0405a92c4a3e371937f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 11:17:21 +1200 Subject: [PATCH 15/86] :bug: Fix stylus-linter warning at Spring Stream --- src/styl/themeSpringStream.styl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index 78b1c1a25..be25d5c16 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -48,6 +48,7 @@ bd = #d6dedd text = #555 snow = #fafafa +introBg = #f2fcfa @require 'hvost.styl' @@ -90,7 +91,7 @@ input[type="reset"], background acttext #bg - background #f2fcfa + background introBg .cards box-sizing border-box From afb6ad285e8f486c8c406ed8f611dfb96147cc8a Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 11:20:07 +1200 Subject: [PATCH 16/86] :sparkles: Add extends to tile layers --- src/js/migration/1.3.2.js | 16 +++++++++++++--- src/node_requires/exporter/rooms.js | 3 ++- src/riotTags/rooms/room-tile-editor.tag | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/js/migration/1.3.2.js b/src/js/migration/1.3.2.js index 6463073c7..87ce9e17f 100644 --- a/src/js/migration/1.3.2.js +++ b/src/js/migration/1.3.2.js @@ -1,11 +1,11 @@ window.migrationProcess = window.migrationProcess || []; -/** - * Project settings got reorganized, logically and visually - */ window.migrationProcess.push({ version: '1.3.2', process: project => new Promise(resolve => { + /** + * Project settings got reorganized, logically and visually + */ const s = project.settings; s.rendering = s.rendering || { @@ -38,6 +38,16 @@ window.migrationProcess.push({ delete s.minifyhtmlcss; // Now doesn't have an option, HTML and CSS are always minified. delete s.minifyjs; // This one never worked properly. + /** + * Tile layers at rooms can now be extended and have an object `exts`. + */ + for (const room of project.rooms) { + for (const layer of room.tiles) { + layer.extends = layer.extends || {}; + delete layer.exts; // Leftovers from development of v1.3.2 + } + } + resolve(); }) }); diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index a2f1e917b..d5776bf37 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -34,7 +34,8 @@ const stringifyRooms = proj => { for (const tileLayer of r.tiles) { const layer = { depth: tileLayer.depth, - tiles: [] + tiles: [], + extends: tileLayer.extends }; for (const tile of tileLayer.tiles) { for (let x = 0; x < tile.grid[2]; x++) { diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index 11598f3ac..20770ec13 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -27,6 +27,8 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix span.act(title="{vocGlob.add}" onclick="{addTileLayer}") svg.feather use(xlink:href="data/icons.svg#plus") + .block + extensions-editor(type="tileLayer" entity="{parent.currentTileLayer.extends}" compact="yep") texture-selector(ref="tilesetPicker" if="{pickingTileset}" oncancelled="{onTilesetCancel}" onselected="{onTilesetSelected}") script. this.parent.tileX = 0; From 019c41f98ac80875d633f1f04e4bd98e8df95e96 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 13:01:37 +1200 Subject: [PATCH 17/86] :zap: Change ct.place.tile to check against collision groups (new!) instead of depth --- app/data/ct.libs/.eslintrc.json | 3 +- app/data/ct.libs/place/DOCS.md | 6 +- app/data/ct.libs/place/index.js | 958 +++++++++--------- .../ct.libs/place/injects/roomoncreate.js | 16 +- app/data/ct.libs/place/module.json | 10 +- app/data/ct.libs/place/types.d.ts | 17 +- 6 files changed, 525 insertions(+), 485 deletions(-) diff --git a/app/data/ct.libs/.eslintrc.json b/app/data/ct.libs/.eslintrc.json index 59a9c57b9..d64caeed7 100644 --- a/app/data/ct.libs/.eslintrc.json +++ b/app/data/ct.libs/.eslintrc.json @@ -4,6 +4,7 @@ "PIXI": true }, "rules": { - "object-shorthand": 0 + "object-shorthand": 0, + "spaced-comment": "off" } } diff --git a/app/data/ct.libs/place/DOCS.md b/app/data/ct.libs/place/DOCS.md index 7cf1464be..076f3e8e0 100644 --- a/app/data/ct.libs/place/DOCS.md +++ b/app/data/ct.libs/place/DOCS.md @@ -21,9 +21,11 @@ If `multiple` is `true`, the function will find all the possible collisions, and Returns `true` if there is a collision between `c1` and `c2` Copies. -## ct.place.tile(me, [x, y, depth]) +## ct.place.tile(me, [x, y, ctype]) -Checks for a collision between a copy `me` and a tile layer of a given `depth`. Depth of a tile layer is equal to what you set in the room editor. If `x` and `y` are skipped, the current coordinates of `me` will be used. +Checks for a collision between a copy `me` and a tile layer of a given collision group (`ctype`). If `ctype` is not set for a tile layer, then ct.place will compare against a tile layer's depth. (This is made for compatibility with older versions of ct.place and ct.js as is.) + +If `x` and `y` are skipped, the current coordinates of `me` will be used. > **Warning:** Each tile is considered a rectangle, and a possible collision mask defined in the graphics asset (in the tileset) is ignored. diff --git a/app/data/ct.libs/place/index.js b/app/data/ct.libs/place/index.js index a1d7462eb..ceb2486bd 100644 --- a/app/data/ct.libs/place/index.js +++ b/app/data/ct.libs/place/index.js @@ -1,473 +1,485 @@ -/* eslint-disable no-underscore-dangle */ -/* global SSCD */ -/* eslint prefer-destructuring: 0 */ -(function (ct) { - const circlePrecision = 16, - twoPi = Math.PI * 0; - var getSSCDShape = function (copy) { - const {shape} = copy, - position = new SSCD.Vector(copy.x, copy.y); - if (shape.type === 'rect') { - if (copy.rotation === 0) { - position.x -= copy.scale.x > 0? (shape.left * copy.scale.x) : (-copy.scale.x * shape.right); - position.y -= copy.scale.y > 0? (shape.top * copy.scale.y) : (-shape.bottom * copy.scale.y); - return new SSCD.Rectangle( - position, - new SSCD.Vector(Math.abs((shape.left + shape.right) * copy.scale.x), Math.abs((shape.bottom + shape.top) * copy.scale.y)) - ); - } - const upperLeft = ct.u.rotate(-shape.left * copy.scale.x, -shape.top * copy.scale.y, copy.rotation), - bottomLeft = ct.u.rotate(-shape.left * copy.scale.x, shape.bottom * copy.scale.y, copy.rotation), - bottomRight = ct.u.rotate(shape.right * copy.scale.x, shape.bottom * copy.scale.y, copy.rotation), - upperRight = ct.u.rotate(shape.right * copy.scale.x, -shape.top * copy.scale.y, copy.rotation); - return new SSCD.LineStrip(position, [ - new SSCD.Vector(upperLeft[0], upperLeft[1]), - new SSCD.Vector(bottomLeft[0], bottomLeft[1]), - new SSCD.Vector(bottomRight[0], bottomRight[1]), - new SSCD.Vector(upperRight[0], upperRight[1]) - ], true); - } - if (shape.type === 'circle') { - if (Math.abs(copy.scale.x) === Math.abs(copy.scale.y)) { - return new SSCD.Circle(position, shape.r * Math.abs(copy.scale.x)); - } - const vertices = []; - for (let i = 0; i < circlePrecision; i++) { - const point = [ - Math.sin(twoPi / circlePrecision * i) * shape.r * copy.scale.x, - Math.cos(twoPi / circlePrecision * i) * shape.r * copy.scale.y - ]; - if (copy.rotation !== 0) { - vertices.push(ct.u.rotate(point[0], point[1], copy.rotation)); - } else { - vertices.push(point); - } - } - return new SSCD.LineStrip(position, vertices, true); - } - if (shape.type === 'strip') { - const vertices = []; - if (copy.rotation !== 0) { - for (const point of shape.points) { - const [x, y] = ct.u.rotate(point.x * copy.scale.x, point.y * copy.scale.y, copy.rotation); - vertices.push(new SSCD.Vector(x, y)); - } - } else { - for (const point of shape.points) { - vertices.push(new SSCD.Vector(point.x * copy.scale.x, point.y * copy.scale.y)); - } - } - return new SSCD.LineStrip(position, vertices, Boolean(shape.closedStrip)); - } - if (shape.type === 'line') { - return new SSCD.Line( - new SSCD.Vector(copy.x + shape.x1 * copy.scale.x, copy.y + shape.y1 * copy.scale.y), - new SSCD.Vector(copy.x + (shape.x2 - shape.x1) * copy.scale.x, copy.y + (shape.y2 - shape.y1) * copy.scale.y) - ); - } - return new SSCD.Circle(position, 0); - }; - - ct.place = { - m: 1, // direction modifier in ct.place.go, - gridX: [/*%gridX%*/][0] || 512, - gridY: [/*%gridY%*/][0] || 512, - grid: {}, - tileGrid: {}, - getHashes(copy) { - var hashes = []; - var x = Math.round(copy.x / ct.place.gridX), - y = Math.round(copy.y / ct.place.gridY), - dx = Math.sign(copy.x - ct.place.gridX * x), - dy = Math.sign(copy.y - ct.place.gridY * y); - hashes.push(`${x}:${y}`); - if (dx) { - hashes.push(`${x+dx}:${y}`); - if (dy) { - hashes.push(`${x+dx}:${y+dy}`); - } - } - if (dy) { - hashes.push(`${x}:${y+dy}`); - } - return hashes; - }, - /** - * Applied to copies in the debug mode. Draws a collision shape - * @this Copy - * @returns {void} - */ - drawDebugGraphic() { - const shape = this._shape || getSSCDShape(this); - const g = this.$cDebugCollision; - const color = this.$cHadCollision? 0x00ff00 : 0x0066ff; - if (shape instanceof SSCD.Rectangle) { - const pos = shape.get_position(), - size = shape.get_size(); - g.lineStyle(2, color) - .drawRect(pos.x - this.x, pos.y - this.y, size.x, size.y); - } else if (shape instanceof SSCD.LineStrip) { - g.lineStyle(2, color) - .moveTo(shape.__points[0].x, shape.__points[0].y); - for (let i = 1; i < shape.__points.length; i++) { - g.lineTo(shape.__points[i].x, shape.__points[i].y); - } - } else if (shape instanceof SSCD.Circle) { - g.lineStyle(2, color) - .drawCircle(0, 0, shape.get_radius()); - } else { - g.lineStyle(4, 0xff0000) - .moveTo(-40, -40) - .lineTo(40, 40,) - .moveTo(-40, 40) - .lineTo(40, -40); - } - }, - collide(c1, c2) { - // ct.place.collide() - // Test collision between two copies - c1._shape = c1._shape || getSSCDShape(c1); - c2._shape = c2._shape || getSSCDShape(c2); - if (c1._shape.__type === 'complex' || c2._shape.__type === 'strip' - || c2._shape.__type === 'complex' || c2._shape.__type === 'strip') { - const aabb1 = c1._shape.get_aabb(), - aabb2 = c2._shape.get_aabb(); - if (!aabb1.intersects(aabb2)) { - return false; - } - } - if (SSCD.CollisionManager.test_collision(c1._shape, c2._shape)) { - if ([/*%debugMode%*/][0]) { - c1.$cHadCollision = true; - c2.$cHadCollision = true; - } - return true; - } - return false; - }, - /** - * Determines if the place in (x,y) is occupied. - * Optionally can take 'ctype' as a filter for obstackles' collision group (not shape type) - * - * @param {Copy} me The object to check collisions on - * @param {number} [x] The x coordinate to check, as if `me` was placed there. - * @param {number} [y] The y coordinate to check, as if `me` was placed there. - * @param {String} [ctype] The collision group to check against - * @param {Boolean} [multiple=false] If it is true, the function will return an array of all the collided objects. - * If it is false (default), it will return a copy with the first collision - * @returns {Copy|Array} The collided copy, or an array of all the detected collisions (if `multiple` is `true`) - */ - occupied(me, x, y, ctype, multiple) { - var oldx = me.x, - oldy = me.y, - shapeCashed = me._shape; - let hashes; - var results; - if (typeof y === 'number') { - me.x = x; - me.y = y; - } else { - ctype = x; - multiple = y; - x = me.x; - y = me.y; - } - if (typeof ctype === 'boolean') { - multiple = ctype; - } - if (oldx !== me.x || oldy !== me.y) { - me._shape = getSSCDShape(me); - hashes = ct.place.getHashes(me); - } else { - hashes = me.$chashes || ct.place.getHashes(me); - } - if (multiple) { - results = []; - } - for (const hash of hashes) { - const array = ct.place.grid[hash]; - if (!array) { - continue; - } - for (let i = 0, l = array.length; i < l; i++) { - if (array[i] !== me && (!ctype || array[i].$ctype === ctype)) { - if (ct.place.collide(me, array[i])) { - /* eslint {"max-depth": "off"} */ - if (!multiple) { - if (oldx !== me.x || oldy !== me.y) { - me.x = oldx; - me.y = oldy; - me._shape = shapeCashed; - } - return array[i]; - } - results.push(array[i]); - } - } - } - } - if (oldx !== me.x || oldy !== me.y) { - me.x = oldx; - me.y = oldy; - me._shape = shapeCashed; - } - if (!multiple) { - return false; - } - return results; - }, - free(me, x, y, ctype) { - return !ct.place.occupied(me, x, y, ctype); - }, - meet(me, x, y, type, multiple) { - // ct.place.meet([, type: Type]) - // detects collision between a given copy and a copy of a certain type - var oldx = me.x, - oldy = me.y, - shapeCashed = me._shape; - let hashes; - var results; - if (typeof y === 'number') { - me.x = x; - me.y = y; - } else { - type = x; - multiple = y; - x = me.x; - y = me.y; - } - if (typeof type === 'boolean') { - multiple = type; - } - if (oldx !== me.x || oldy !== me.y) { - me._shape = getSSCDShape(me); - hashes = ct.place.getHashes(me); - } else { - hashes = me.$chashes || ct.place.getHashes(me); - } - if (multiple) { - results = []; - } - for (const hash of hashes) { - const array = ct.place.grid[hash]; - if (!array) { - continue; - } - for (let i = 0, l = array.length; i < l; i++) { - if (array[i].type === type && array[i] !== me && ct.place.collide(me, array[i])) { - if (!multiple) { - if (oldx !== me.x || oldy !== me.y) { - me._shape = shapeCashed; - me.x = oldx; - me.y = oldy; - } - return array[i]; - } - results.push(array[i]); - } - } - } - if (oldx !== me.x || oldy !== me.y) { - me.x = oldx; - me.y = oldy; - me._shape = shapeCashed; - } - if (!multiple) { - return false; - } - return results; - }, - tile(me, x, y, depth) { - if (!me.shape || !me.shape.type) { - return false; - } - var oldx = me.x, - oldy = me.y, - shapeCashed = me._shape; - let hashes; - if (y !== void 0) { - me.x = x; - me.y = y; - } else { - depth = x; - x = me.x; - y = me.y; - } - if (oldx !== me.x || oldy !== me.y) { - me._shape = getSSCDShape(me); - hashes = ct.place.getHashes(me); - } else { - hashes = me.$chashes || ct.place.getHashes(me); - } - for (const hash of hashes) { - const array = ct.place.tileGrid[hash]; - if (!array) { - continue; - } - for (let i = 0, l = array.length; i < l; i++) { - const tile = array[i]; - if (!depth || tile.depth === depth && ct.place.collide(tile, me)) { - if (oldx !== me.x || oldy !== me.y) { - me.x = oldx; - me.y = oldy; - me._shape = shapeCashed; - } - return true; - } - } - } - if (oldx !== me.x || oldy !== me.y) { - me.x = oldx; - me.y = oldy; - me._shape = shapeCashed; - } - return false; - }, - lastdist: null, - nearest(x, y, type) { - // ct.place.nearest() - if (ct.types.list[type].length > 0) { - var dist = Math.hypot(x-ct.types.list[type][0].x, y-ct.types.list[type][0].y); - var inst = ct.types.list[type][0]; - for (const copy of ct.types.list[type]) { - if (Math.hypot(x-copy.x, y-copy.y) < dist) { - dist = Math.hypot(x-copy.x, y-copy.y); - inst = copy; - } - } - ct.place.lastdist = dist; - return inst; - } - return false; - }, - furthest(x, y, type) { - // ct.place.furthest() - if (ct.types.list[type].length > 0) { - - var dist = Math.hypot(x-ct.types.list[type][0].x, y-ct.types.list[type][0].y); - var inst = ct.types.list[type][0]; - for (const copy of ct.types.list[type]) { - if (Math.hypot(x - copy.x, y - copy.y) > dist) { - dist = Math.hypot(x - copy.x, y - copy.y); - inst = copy; - } - } - ct.place.lastdist = dist; - return inst; - } - return false; - }, - moveAlong(me, dir, length, ctype, precision) { - if (typeof ctype === 'number') { - precision = ctype; - ctype = void 0; - } - precision = Math.abs(precision || 1); - if (length < 0) { - length *= -1; - dir += 180; - } - var dx = Math.cos(dir*Math.PI/-180) * precision, - dy = Math.sin(dir*Math.PI/-180) * precision; - for (let i = 0; i < length; i+= precision) { - const occupied = ct.place.occupied(me, me.x + dx, me.y + dy, ctype); - if (!occupied) { - me.x += dx; - me.y += dy; - delete me._shape; - } else { - return occupied; - } - } - return false; - }, - go(me, x, y, length, ctype) { - // ct.place.go([, ctype: String]) - // tries to reach the target with a simple obstacle avoidance algorithm - - // if we are too close to the destination, exit - if (ct.u.pdc(me.x, me.y, x, y) < length) { - if (ct.place.free(me, x, y, ctype)) { - me.x = x; - me.y = y; - delete me._shape; - } - return; - } - var dir = ct.u.pdn(me.x, me.y, x, y); - - //if there are no obstackles in front of us, go forward - if (ct.place.free(me, me.x+ct.u.ldx(length, dir), me.y+ct.u.ldy(length, dir), ctype)) { - me.x += ct.u.ldx(length, dir); - me.y += ct.u.ldy(length, dir); - delete me._shape; - me.dir = dir; - // otherwise, try to change direction by 30...60...90 degrees. - // Direction changes over time (ct.place.m). - } else { - for (var i = -1; i <= 1; i+= 2) { - for (var j = 30; j < 150; j += 30) { - if (ct.place.free(me, me.x+ct.u.ldx(length, dir+j * ct.place.m*i), me.y+ct.u.ldy(length, dir+j * ct.place.m*i), ctype)) { - me.x += ct.u.ldx(length, dir+j * ct.place.m*i); - me.y += ct.u.ldy(length, dir+j * ct.place.m*i); - delete me._shape; - me.dir = dir+j * ct.place.m*i; - return; - } - } - } - } - }, - /** - * Throws a ray from point (x1, y1) to (x2, y2), returning all the instances that touched the ray. - * The first copy in the returned array is the closest copy, the last one is the furthest. - * - * @param {number} x1 A horizontal coordinate of the starting point of the ray. - * @param {number} y1 A vertical coordinate of the starting point of the ray. - * @param {number} x2 A horizontal coordinate of the ending point of the ray. - * @param {number} y2 A vertical coordinate of the ending point of the ray. - * @param {String} [ctype] An optional collision group to trace against. If omitted, will trace through all the copies in the current room. - * - * @returns {Array} Array of all the copies that touched the ray - */ - trace(x1, y1, x2, y2, ctype) { - var copies = [], - ray = { - x: 0, - y: 0, - scale: { - x: 1, - y: 1 - }, - rotation: 0, - shape: { - type: 'line', - x1: x1, - y1: y1, - x2: x2, - y2: y2 - } - }; - for (var i in ct.stack) { - if (!ctype || ct.stack[i].ctype === ctype) { - if (ct.place.collide(ray, ct.stack[i])) { - copies.push(ct.stack[i]); - } - } - } - if (copies.length > 1) { - copies.sort(function (a, b) { - var dist1, dist2; - dist1 = ct.u.pdc(x1, y1, a.x, a.y); - dist2 = ct.u.pdc(x1, y1, b.x, b.y); - return dist1 - dist2; - }); - } - return copies; - } - }; - // a magic procedure which tells 'go' function to change its direction - setInterval(function() { - ct.place.m *= -1; - }, 789); -})(ct); +/* eslint-disable no-underscore-dangle */ +/* global SSCD */ +/* eslint prefer-destructuring: 0 */ +(function ctPlace(ct) { + const circlePrecision = 16, + twoPi = Math.PI * 0; + var getSSCDShape = function (copy) { + const {shape} = copy, + position = new SSCD.Vector(copy.x, copy.y); + if (shape.type === 'rect') { + if (copy.rotation === 0) { + position.x -= copy.scale.x > 0 ? (shape.left * copy.scale.x) : (-copy.scale.x * shape.right); + position.y -= copy.scale.y > 0 ? (shape.top * copy.scale.y) : (-shape.bottom * copy.scale.y); + return new SSCD.Rectangle( + position, + new SSCD.Vector(Math.abs((shape.left + shape.right) * copy.scale.x), Math.abs((shape.bottom + shape.top) * copy.scale.y)) + ); + } + const upperLeft = ct.u.rotate(-shape.left * copy.scale.x, -shape.top * copy.scale.y, copy.rotation), + bottomLeft = ct.u.rotate(-shape.left * copy.scale.x, shape.bottom * copy.scale.y, copy.rotation), + bottomRight = ct.u.rotate(shape.right * copy.scale.x, shape.bottom * copy.scale.y, copy.rotation), + upperRight = ct.u.rotate(shape.right * copy.scale.x, -shape.top * copy.scale.y, copy.rotation); + return new SSCD.LineStrip(position, [ + new SSCD.Vector(upperLeft[0], upperLeft[1]), + new SSCD.Vector(bottomLeft[0], bottomLeft[1]), + new SSCD.Vector(bottomRight[0], bottomRight[1]), + new SSCD.Vector(upperRight[0], upperRight[1]) + ], true); + } + if (shape.type === 'circle') { + if (Math.abs(copy.scale.x) === Math.abs(copy.scale.y)) { + return new SSCD.Circle(position, shape.r * Math.abs(copy.scale.x)); + } + const vertices = []; + for (let i = 0; i < circlePrecision; i++) { + const point = [ + Math.sin(twoPi / circlePrecision * i) * shape.r * copy.scale.x, + Math.cos(twoPi / circlePrecision * i) * shape.r * copy.scale.y + ]; + if (copy.rotation !== 0) { + vertices.push(ct.u.rotate(point[0], point[1], copy.rotation)); + } else { + vertices.push(point); + } + } + return new SSCD.LineStrip(position, vertices, true); + } + if (shape.type === 'strip') { + const vertices = []; + if (copy.rotation !== 0) { + for (const point of shape.points) { + const [x, y] = ct.u.rotate(point.x * copy.scale.x, point.y * copy.scale.y, copy.rotation); + vertices.push(new SSCD.Vector(x, y)); + } + } else { + for (const point of shape.points) { + vertices.push(new SSCD.Vector(point.x * copy.scale.x, point.y * copy.scale.y)); + } + } + return new SSCD.LineStrip(position, vertices, Boolean(shape.closedStrip)); + } + if (shape.type === 'line') { + return new SSCD.Line( + new SSCD.Vector(copy.x + shape.x1 * copy.scale.x, copy.y + shape.y1 * copy.scale.y), + new SSCD.Vector(copy.x + (shape.x2 - shape.x1) * copy.scale.x, copy.y + (shape.y2 - shape.y1) * copy.scale.y) + ); + } + return new SSCD.Circle(position, 0); + }; + + ct.place = { + m: 1, // direction modifier in ct.place.go, + gridX: [/*%gridX%*/][0] || 512, + gridY: [/*%gridY%*/][0] || 512, + grid: {}, + tileGrid: {}, + getHashes(copy) { + var hashes = []; + var x = Math.round(copy.x / ct.place.gridX), + y = Math.round(copy.y / ct.place.gridY), + dx = Math.sign(copy.x - ct.place.gridX * x), + dy = Math.sign(copy.y - ct.place.gridY * y); + hashes.push(`${x}:${y}`); + if (dx) { + hashes.push(`${x + dx}:${y}`); + if (dy) { + hashes.push(`${x + dx}:${y + dy}`); + } + } + if (dy) { + hashes.push(`${x}:${y + dy}`); + } + return hashes; + }, + /** + * Applied to copies in the debug mode. Draws a collision shape + * @this Copy + * @returns {void} + */ + drawDebugGraphic() { + const shape = this._shape || getSSCDShape(this); + const g = this.$cDebugCollision; + const color = this.$cHadCollision ? 0x00ff00 : 0x0066ff; + if (shape instanceof SSCD.Rectangle) { + const pos = shape.get_position(), + size = shape.get_size(); + g.lineStyle(2, color) + .drawRect(pos.x - this.x, pos.y - this.y, size.x, size.y); + } else if (shape instanceof SSCD.LineStrip) { + g.lineStyle(2, color) + .moveTo(shape.__points[0].x, shape.__points[0].y); + for (let i = 1; i < shape.__points.length; i++) { + g.lineTo(shape.__points[i].x, shape.__points[i].y); + } + } else if (shape instanceof SSCD.Circle) { + g.lineStyle(2, color) + .drawCircle(0, 0, shape.get_radius()); + } else { + g.lineStyle(4, 0xff0000) + .moveTo(-40, -40) + .lineTo(40, 40) + .moveTo(-40, 40) + .lineTo(40, -40); + } + }, + drawDebugTileGraphic(tile) { + const g = this.$cDebugCollision; + const color = 0x0066ff; + g.lineStyle(2, color) + .drawRect(tile.x - this.x, tile.y - this.y, tile.width, tile.height); + }, + collide(c1, c2) { + // ct.place.collide() + // Test collision between two copies + c1._shape = c1._shape || getSSCDShape(c1); + c2._shape = c2._shape || getSSCDShape(c2); + if (c1._shape.__type === 'complex' || c2._shape.__type === 'strip' || + c2._shape.__type === 'complex' || c2._shape.__type === 'strip') { + const aabb1 = c1._shape.get_aabb(), + aabb2 = c2._shape.get_aabb(); + if (!aabb1.intersects(aabb2)) { + return false; + } + } + if (SSCD.CollisionManager.test_collision(c1._shape, c2._shape)) { + if ([/*%debugMode%*/][0]) { + c1.$cHadCollision = true; + c2.$cHadCollision = true; + } + return true; + } + return false; + }, + /** + * Determines if the place in (x,y) is occupied. + * Optionally can take 'ctype' as a filter for obstackles' collision group (not shape type) + * + * @param {Copy} me The object to check collisions on + * @param {number} [x] The x coordinate to check, as if `me` was placed there. + * @param {number} [y] The y coordinate to check, as if `me` was placed there. + * @param {String} [ctype] The collision group to check against + * @param {Boolean} [multiple=false] If it is true, the function will return an array of all the collided objects. + * If it is false (default), it will return a copy with the first collision + * @returns {Copy|Array} The collided copy, or an array of all the detected collisions (if `multiple` is `true`) + */ + occupied(me, x, y, ctype, multiple) { + var oldx = me.x, + oldy = me.y, + shapeCashed = me._shape; + let hashes; + var results; + if (typeof y === 'number') { + me.x = x; + me.y = y; + } else { + ctype = x; + multiple = y; + x = me.x; + y = me.y; + } + if (typeof ctype === 'boolean') { + multiple = ctype; + } + if (oldx !== me.x || oldy !== me.y) { + me._shape = getSSCDShape(me); + hashes = ct.place.getHashes(me); + } else { + hashes = me.$chashes || ct.place.getHashes(me); + } + if (multiple) { + results = []; + } + for (const hash of hashes) { + const array = ct.place.grid[hash]; + if (!array) { + continue; + } + for (let i = 0, l = array.length; i < l; i++) { + if (array[i] !== me && (!ctype || array[i].$ctype === ctype)) { + if (ct.place.collide(me, array[i])) { + /* eslint {"max-depth": "off"} */ + if (!multiple) { + if (oldx !== me.x || oldy !== me.y) { + me.x = oldx; + me.y = oldy; + me._shape = shapeCashed; + } + return array[i]; + } + results.push(array[i]); + } + } + } + } + if (oldx !== me.x || oldy !== me.y) { + me.x = oldx; + me.y = oldy; + me._shape = shapeCashed; + } + if (!multiple) { + return false; + } + return results; + }, + free(me, x, y, ctype) { + return !ct.place.occupied(me, x, y, ctype); + }, + meet(me, x, y, type, multiple) { + // ct.place.meet([, type: Type]) + // detects collision between a given copy and a copy of a certain type + var oldx = me.x, + oldy = me.y, + shapeCashed = me._shape; + let hashes; + var results; + if (typeof y === 'number') { + me.x = x; + me.y = y; + } else { + type = x; + multiple = y; + x = me.x; + y = me.y; + } + if (typeof type === 'boolean') { + multiple = type; + } + if (oldx !== me.x || oldy !== me.y) { + me._shape = getSSCDShape(me); + hashes = ct.place.getHashes(me); + } else { + hashes = me.$chashes || ct.place.getHashes(me); + } + if (multiple) { + results = []; + } + for (const hash of hashes) { + const array = ct.place.grid[hash]; + if (!array) { + continue; + } + for (let i = 0, l = array.length; i < l; i++) { + if (array[i].type === type && array[i] !== me && ct.place.collide(me, array[i])) { + if (!multiple) { + if (oldx !== me.x || oldy !== me.y) { + me._shape = shapeCashed; + me.x = oldx; + me.y = oldy; + } + return array[i]; + } + results.push(array[i]); + } + } + } + if (oldx !== me.x || oldy !== me.y) { + me.x = oldx; + me.y = oldy; + me._shape = shapeCashed; + } + if (!multiple) { + return false; + } + return results; + }, + tile(me, x, y, ctype) { + if (!me.shape || !me.shape.type) { + return false; + } + var oldx = me.x, + oldy = me.y, + shapeCashed = me._shape; + let hashes; + if (y !== void 0) { + me.x = x; + me.y = y; + } else { + ctype = x; + x = me.x; + y = me.y; + } + if (oldx !== me.x || oldy !== me.y) { + me._shape = getSSCDShape(me); + hashes = ct.place.getHashes(me); + } else { + hashes = me.$chashes || ct.place.getHashes(me); + } + for (const hash of hashes) { + const array = ct.place.tileGrid[hash]; + if (!array) { + continue; + } + for (let i = 0, l = array.length; i < l; i++) { + const tile = array[i]; + const tileMatches = typeof ctype === 'string' ? tile.ctype === ctype : tile.depth === ctype; + if ((!ctype || tileMatches) && ct.place.collide(tile, me)) { + if (oldx !== me.x || oldy !== me.y) { + me.x = oldx; + me.y = oldy; + me._shape = shapeCashed; + } + return true; + } + } + } + if (oldx !== me.x || oldy !== me.y) { + me.x = oldx; + me.y = oldy; + me._shape = shapeCashed; + } + return false; + }, + lastdist: null, + nearest(x, y, type) { + // ct.place.nearest() + if (ct.types.list[type].length > 0) { + var dist = Math.hypot(x - ct.types.list[type][0].x, y - ct.types.list[type][0].y); + var inst = ct.types.list[type][0]; + for (const copy of ct.types.list[type]) { + if (Math.hypot(x - copy.x, y - copy.y) < dist) { + dist = Math.hypot(x - copy.x, y - copy.y); + inst = copy; + } + } + ct.place.lastdist = dist; + return inst; + } + return false; + }, + furthest(x, y, type) { + // ct.place.furthest() + if (ct.types.list[type].length > 0) { + var dist = Math.hypot(x - ct.types.list[type][0].x, y - ct.types.list[type][0].y); + var inst = ct.types.list[type][0]; + for (const copy of ct.types.list[type]) { + if (Math.hypot(x - copy.x, y - copy.y) > dist) { + dist = Math.hypot(x - copy.x, y - copy.y); + inst = copy; + } + } + ct.place.lastdist = dist; + return inst; + } + return false; + }, + moveAlong(me, dir, length, ctype, precision) { + if (typeof ctype === 'number') { + precision = ctype; + ctype = void 0; + } + precision = Math.abs(precision || 1); + if (length < 0) { + length *= -1; + dir += 180; + } + var dx = Math.cos(dir * Math.PI / -180) * precision, + dy = Math.sin(dir * Math.PI / -180) * precision; + for (let i = 0; i < length; i += precision) { + const occupied = ct.place.occupied(me, me.x + dx, me.y + dy, ctype); + if (!occupied) { + me.x += dx; + me.y += dy; + delete me._shape; + } else { + return occupied; + } + } + return false; + }, + go(me, x, y, length, ctype) { + // ct.place.go([, ctype: String]) + // tries to reach the target with a simple obstacle avoidance algorithm + + // if we are too close to the destination, exit + if (ct.u.pdc(me.x, me.y, x, y) < length) { + if (ct.place.free(me, x, y, ctype)) { + me.x = x; + me.y = y; + delete me._shape; + } + return; + } + var dir = ct.u.pdn(me.x, me.y, x, y); + + //if there are no obstackles in front of us, go forward + let projectedX = me.x + ct.u.ldx(length, dir), + projectedY = me.y + ct.u.ldy(length, dir); + if (ct.place.free(me, projectedX, projectedY, ctype)) { + me.x = projectedX; + me.y = projectedY; + delete me._shape; + me.dir = dir; + // otherwise, try to change direction by 30...60...90 degrees. + // Direction changes over time (ct.place.m). + } else { + for (var i = -1; i <= 1; i += 2) { + for (var j = 30; j < 150; j += 30) { + projectedX = me.x + ct.u.ldx(length, dir + j * ct.place.m * i); + projectedY = me.y + ct.u.ldy(length, dir + j * ct.place.m * i); + if (ct.place.free(me, projectedX, projectedY, ctype)) { + me.x = projectedX; + me.y = projectedY; + delete me._shape; + me.dir = dir + j * ct.place.m * i; + return; + } + } + } + } + }, + /** + * Throws a ray from point (x1, y1) to (x2, y2), returning all the copies + * that touched the ray. The first copy in the returned array is the closest copy, + * the last one is the furthest. + * + * @param {number} x1 A horizontal coordinate of the starting point of the ray. + * @param {number} y1 A vertical coordinate of the starting point of the ray. + * @param {number} x2 A horizontal coordinate of the ending point of the ray. + * @param {number} y2 A vertical coordinate of the ending point of the ray. + * @param {String} [ctype] An optional collision group to trace against. + * If omitted, will trace through all the copies in the current room. + * + * @returns {Array} Array of all the copies that touched the ray + */ + trace(x1, y1, x2, y2, ctype) { + var copies = [], + ray = { + x: 0, + y: 0, + scale: { + x: 1, + y: 1 + }, + rotation: 0, + shape: { + type: 'line', + x1: x1, + y1: y1, + x2: x2, + y2: y2 + } + }; + for (var i in ct.stack) { + if (!ctype || ct.stack[i].ctype === ctype) { + if (ct.place.collide(ray, ct.stack[i])) { + copies.push(ct.stack[i]); + } + } + } + if (copies.length > 1) { + copies.sort(function sortCopies(a, b) { + var dist1, dist2; + dist1 = ct.u.pdc(x1, y1, a.x, a.y); + dist2 = ct.u.pdc(x1, y1, b.x, b.y); + return dist1 - dist2; + }); + } + return copies; + } + }; + // a magic procedure which tells 'go' function to change its direction + setInterval(function switchCtPlaceGoDirection() { + ct.place.m *= -1; + }, 789); +})(ct); diff --git a/app/data/ct.libs/place/injects/roomoncreate.js b/app/data/ct.libs/place/injects/roomoncreate.js index bf94887ba..0130b2882 100644 --- a/app/data/ct.libs/place/injects/roomoncreate.js +++ b/app/data/ct.libs/place/injects/roomoncreate.js @@ -1,11 +1,16 @@ /* global SSCD */ ct.place.tileGrid = {}; if (ct.types.list.TILELAYER) { + console.log(ct.types.list.TILELAYER.length); for (const layer of ct.types.list.TILELAYER) { for (let i = 0, l = layer.tiles.length; i < l; i++) { const t = layer.tiles[i]; // eslint-disable-next-line no-underscore-dangle - t._shape = new SSCD.Rectangle(new SSCD.Vector(t.x, t.y), new SSCD.Vector(t.width, t.height)); + t._shape = new SSCD.Rectangle( + new SSCD.Vector(t.x, t.y), + new SSCD.Vector(t.width, t.height) + ); + t.ctype = layer.ctype; t.$chashes = ct.place.getHashes(t); /* eslint max-depth: 0 */ for (const hash of t.$chashes) { @@ -17,5 +22,14 @@ if (ct.types.list.TILELAYER) { } t.depth = layer.depth; } + if ([/*%debugMode%*/][0]) { + for (let i = 0; i < layer.tiles.length; i++) { + const pixiTile = layer.children[i], + logicTile = layer.tiles[i]; + pixiTile.$cDebugCollision = new PIXI.Graphics(); + ct.place.drawDebugTileGraphic.apply(pixiTile, [logicTile]); + pixiTile.addChild(pixiTile.$cDebugCollision); + } + } } } diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 6abb29741..457bfb795 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.place", - "version": "3.0.0", + "version": "3.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" @@ -40,6 +40,14 @@ "name": "Collision group", "type": "text", "collect": true, + "collectScope": "place::ctype", + "key": "ctype" + }], + "tileLayerExtends": [{ + "name": "Collision group", + "type": "text", + "collect": true, + "collectScope": "place::ctype", "key": "ctype" }] } diff --git a/app/data/ct.libs/place/types.d.ts b/app/data/ct.libs/place/types.d.ts index 43dddf7c3..cde8d66d2 100644 --- a/app/data/ct.libs/place/types.d.ts +++ b/app/data/ct.libs/place/types.d.ts @@ -103,20 +103,23 @@ declare namespace ct { function meet(me: Copy, type: string, multiple?: false|void): Copy | false; /** - * Checks for a collision between a copy `me` and a tile layer of a given `depth`. - * Depth of a tile layer is equal to what you set in the room editor. + * Checks for a collision between a copy `me` and a tile layer of a given collision group (`ctype`). + * If `ctype` is not set for a tile layer, then ct.place will compare against a tile layer's depth. + * + * If `x` and `y` are skipped, the current coordinates of `me` will be used. + * * Each tile is considered a rectangle, and a possible collision mask defined in the graphics asset * (in the tileset) is ignored. If `x` and `y` are skipped, the current coordinates of `me` will be used. * * This method returns either `true` (a copy collides with a tile layer) or `false` (no collision). * * @param {Copy} me The copy to calculate collisions for - * @param {number} [x] The x coordinate to check, as if `me` was placed there - * @param {number} [y] The y coordinate to check, as if `me` was placed there - * @param {number} depth The depth of a tile layer to test collisions against + * @param {number} [x] The x coordinate to check, as if `me` was placed there. + * @param {number} [y] The y coordinate to check, as if `me` was placed there. + * @param {number} ctype The collision group of tile layers to test against. */ - function tile(me: Copy, x: number, y: number, depth: number): boolean; - function tile(me: Copy, depth: number): boolean; + function tile(me: Copy, x: number, y: number, ctype: string): boolean; + function tile(me: Copy, ctype: string): boolean; /** * Returns the latest distance after calling `ct.place.furthest` or `ct.place.nearest`. From 81129ec829c914d26c5152b4445210ad843f3750 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 13:02:15 +1200 Subject: [PATCH 18/86] :bug: Do not reuse tiles directly from room templates Closes #191 --- app/data/ct.release/types.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/data/ct.release/types.js b/app/data/ct.release/types.js index 14dc33b7c..e7fad88ac 100644 --- a/app/data/ct.release/types.js +++ b/app/data/ct.release/types.js @@ -67,7 +67,12 @@ class Tileset extends PIXI.Container { constructor(template) { super(); this.depth = template.depth; - this.tiles = template.tiles; + this.tiles = template.tiles.map(tile => ({ + ...tile + })); + if (template.extends) { + Object.assign(this, template.extends); + } ct.types.list.TILELAYER.push(this); for (let i = 0, l = template.tiles.length; i < l; i++) { const textures = ct.res.getTexture(template.tiles[i].texture); From c67436704a533d648fdc8633d87a4bf8e439d853 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 13:10:31 +1200 Subject: [PATCH 19/86] :bug: Remove console.log at ct.place --- app/data/ct.libs/place/injects/roomoncreate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/data/ct.libs/place/injects/roomoncreate.js b/app/data/ct.libs/place/injects/roomoncreate.js index 0130b2882..ce117511c 100644 --- a/app/data/ct.libs/place/injects/roomoncreate.js +++ b/app/data/ct.libs/place/injects/roomoncreate.js @@ -1,7 +1,6 @@ /* global SSCD */ ct.place.tileGrid = {}; if (ct.types.list.TILELAYER) { - console.log(ct.types.list.TILELAYER.length); for (const layer of ct.types.list.TILELAYER) { for (let i = 0, l = layer.tiles.length; i < l; i++) { const t = layer.tiles[i]; From 2d4d912a3ae1ce4b5324da866e65528d049a4dfd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 13:27:36 +1200 Subject: [PATCH 20/86] :zap: Make the structural behavior of TileLayer consistent. Fixes drawing issues with tiles and ct.place debug mode --- .../ct.libs/place/injects/roomoncreate.js | 2 +- app/data/ct.release/types.js | 30 ++++--------------- app/package.json | 2 +- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/app/data/ct.libs/place/injects/roomoncreate.js b/app/data/ct.libs/place/injects/roomoncreate.js index ce117511c..6d7488a49 100644 --- a/app/data/ct.libs/place/injects/roomoncreate.js +++ b/app/data/ct.libs/place/injects/roomoncreate.js @@ -23,7 +23,7 @@ if (ct.types.list.TILELAYER) { } if ([/*%debugMode%*/][0]) { for (let i = 0; i < layer.tiles.length; i++) { - const pixiTile = layer.children[i], + const pixiTile = layer.pixiTiles[i], logicTile = layer.tiles[i]; pixiTile.$cDebugCollision = new PIXI.Graphics(); ct.place.drawDebugTileGraphic.apply(pixiTile, [logicTile]); diff --git a/app/data/ct.release/types.js b/app/data/ct.release/types.js index e7fad88ac..ccc2d69d3 100644 --- a/app/data/ct.release/types.js +++ b/app/data/ct.release/types.js @@ -70,6 +70,7 @@ class Tileset extends PIXI.Container { this.tiles = template.tiles.map(tile => ({ ...tile })); + this.pixiTiles = []; if (template.extends) { Object.assign(this, template.extends); } @@ -79,48 +80,30 @@ class Tileset extends PIXI.Container { const sprite = new PIXI.Sprite(textures[template.tiles[i].frame]); sprite.anchor.x = sprite.anchor.y = 0; this.addChild(sprite); + this.pixiTiles.push(sprite); sprite.x = template.tiles[i].x; sprite.y = template.tiles[i].y; } + // Divide tiles into a grid of larger cells so that we can cache these cells as const bounds = this.getLocalBounds(); const cols = Math.ceil(bounds.width / 1024), rows = Math.ceil(bounds.height / 1024); - if (cols < 2 && rows < 2) { - if (this.width > 0 && this.height > 0) { - this.cacheAsBitmap = true; - } - return this; - } - /*const mask = new PIXI.Graphics(); - mask.lineStyle(0); - mask.beginFill(0xffffff); - mask.drawRect(0, 0, 1024, 1024); - mask.endFill();*/ this.cells = []; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const cell = new PIXI.Container(); - //cell.x = x * 1024 + bounds.x; - //cell.y = y * 1024 + bounds.y; this.cells.push(cell); } } - for (let i = 0, l = template.tiles.length; i < l; i++) { + for (let i = 0, l = this.tiles.length; i < l; i++) { const tile = this.children[0], x = Math.floor((tile.x - bounds.x) / 1024), y = Math.floor((tile.y - bounds.y) / 1024); this.cells[y * cols + x].addChild(tile); - /*if (tile.x - x * 1024 + tile.width > 1024) { - this.cells[y*cols + x + 1].addChild(tile); - if (tile.y - y * 1024 + tile.height > 1024) { - this.cells[(y+1)*cols + x + 1].addChild(tile); - } - } - if (tile.y - y * 1024 + tile.height > 1024) { - this.cells[(y+1)*cols + x].addChild(tile); - }*/ } this.removeChildren(); + + // Filter out empty cells, cache filled ones for (let i = 0, l = this.cells.length; i < l; i++) { if (this.cells[i].children.length === 0) { this.cells.splice(i, 1); @@ -128,7 +111,6 @@ class Tileset extends PIXI.Container { l--; continue; } - //this.cells[i].mask = mask; this.addChild(this.cells[i]); this.cells[i].cacheAsBitmap = true; } diff --git a/app/package.json b/app/package.json index b15df2b3e..0174435fe 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "main": "index.html", "name": "ctjs", "description": "ct.js — a free 2D game engine", - "version": "1.3.1", + "version": "1.3.2", "homepage": "https://ctjs.rocks/", "author": { "name": "Cosmo Myzrail Gorynych", From 53f2aa251336d877c3e0a052d49a38de01b6fcbf Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 17:11:28 +1200 Subject: [PATCH 21/86] :zap: Use less restrictive YAML reader/writer to allow some minor save file errors --- src/js/loadProject.js | 2 +- src/riotTags/main-menu.tag | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/loadProject.js b/src/js/loadProject.js index 055c1e71a..03c79f2bf 100644 --- a/src/js/loadProject.js +++ b/src/js/loadProject.js @@ -150,7 +150,7 @@ } else { try { const YAML = require('js-yaml'); - projectData = YAML.safeLoad(textProjData); + projectData = YAML.load(textProjData); } catch (e) { // whoopsie, wrong window // eslint-disable-next-line no-console diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index faee29155..c12de9a34 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -123,7 +123,7 @@ main-menu.flexcol }; this.saveProject = () => { const YAML = require('js-yaml'); - const projectYAML = YAML.safeDump(global.currentProject); + const projectYAML = YAML.dump(global.currentProject); return fs.outputFile(global.projdir + '.ict', projectYAML) .then(() => { alertify.success(window.languageJSON.common.savedcomm, 'success', 3000); @@ -137,7 +137,7 @@ main-menu.flexcol this.saveRecovery = () => { if (global.currentProject) { const YAML = require('js-yaml'); - const recoveryYAML = YAML.safeDump(global.currentProject); + const recoveryYAML = YAML.dump(global.currentProject); fs.outputFile(global.projdir + '.ict.recovery', recoveryYAML); } this.saveRecoveryDebounce(); From 48dbb06173a6933e97308a0325d244ff4b1aba0a Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 17:15:27 +1200 Subject: [PATCH 22/86] :zap: Fully wrap the saveProject method into try/catch block --- src/riotTags/main-menu.tag | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index c12de9a34..fc33da79a 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -121,18 +121,19 @@ main-menu.flexcol this.refs.catMenu.toggle(); } }; - this.saveProject = () => { - const YAML = require('js-yaml'); - const projectYAML = YAML.dump(global.currentProject); - return fs.outputFile(global.projdir + '.ict', projectYAML) - .then(() => { - alertify.success(window.languageJSON.common.savedcomm, 'success', 3000); + this.saveProject = async () => { + try { + const YAML = require('js-yaml'); + const projectYAML = YAML.dump(global.currentProject); + await fs.outputFile(global.projdir + '.ict', projectYAML); this.saveRecoveryDebounce(); fs.remove(global.projdir + '.ict.recovery') .catch(console.error); glob.modified = false; - }) - .catch(alertify.error); + alertify.success(window.languageJSON.common.savedcomm, 'success', 3000); + } catch (e) { + alertify.error(e); + } }; this.saveRecovery = () => { if (global.currentProject) { From 175b467f17396b4e20b0174f309fd20a8594cd94 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Jun 2020 17:23:45 +1200 Subject: [PATCH 23/86] :zap: Update dependencies --- app/package-lock.json | 77 ++++++++++++++++++++----------------------- app/package.json | 12 +++---- package-lock.json | 51 ++++++++++++++-------------- package.json | 4 +-- 4 files changed, 67 insertions(+), 77 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index b14316752..dcf476895 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "ctjs", - "version": "1.3.1", + "version": "1.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2241,9 +2241,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", - "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "requires": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -2252,9 +2252,9 @@ }, "dependencies": { "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "jsonfile": { "version": "6.0.1", @@ -2289,9 +2289,9 @@ } }, "fuse.js": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.6.tgz", - "integrity": "sha512-H6aJY4UpLFwxj1+5nAvufom5b2BT2v45P1MkPvdGIK8fWjQx/7o6tTT1+ALV0yawQvbmvCF0ufl2et8eJ7v7Cg==" + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", + "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==" }, "galactus": { "version": "0.2.1", @@ -2543,9 +2543,9 @@ "integrity": "sha512-ta9UdV60xVZk/ZafFtSFslQaE76SvNkcs1r73d2PVR21zVzx9xuYv9tNe4MxA1NN7WoeCc2RjGot3Bz1eHDx3Q==" }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2750,23 +2750,16 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } + "minimist": "^1.2.5" } }, "monaco-editor": { @@ -2913,9 +2906,9 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "param-case": { "version": "2.1.1", @@ -2983,9 +2976,9 @@ "optional": true }, "pixi-particles": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pixi-particles/-/pixi-particles-4.2.0.tgz", - "integrity": "sha512-20aNuzrKtkpZZA8m4+fp56RwZMPaWnRkoTGPr6U+35XyRJoGlH13adSLiknL3TU0FGmGHKUw4fYAhn41Ujq3xQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/pixi-particles/-/pixi-particles-4.2.1.tgz", + "integrity": "sha512-8Hp7HZU+hYFHVOAigLf4gzimDusDK1ecDGjnOsg5JhnBT9p7V1aNr6U7DuHyDnGGGk6IoLlovggx7/WEMNNe4A==" }, "pixi.js": { "version": "5.1.2", @@ -3380,9 +3373,9 @@ } }, "ttf2woff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ttf2woff/-/ttf2woff-2.0.1.tgz", - "integrity": "sha1-hxgyJAAksJ25VwkEx8GSi4BXyWk=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ttf2woff/-/ttf2woff-2.0.2.tgz", + "integrity": "sha512-X68badwBjAy/+itU49scLjXUL094up+rHuYk+YAOTTBYSUMOmLZ7VyhZJuqQESj1gnyLAC2/5V8Euv+mExmyPA==", "requires": { "argparse": "^1.0.6", "microbuffer": "^1.0.0", @@ -3428,9 +3421,9 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "unzipper": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", - "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", "requires": { "big-integer": "^1.6.17", "binary": "~0.3.0", @@ -3445,9 +3438,9 @@ }, "dependencies": { "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" } } }, diff --git a/app/package.json b/app/package.json index 0174435fe..4ec0f7516 100644 --- a/app/package.json +++ b/app/package.json @@ -56,21 +56,21 @@ "archiver": "^3.1.1", "csswring": "7.0.0", "electron-packager": "^14.2.1", - "fs-extra": "^9.0.0", - "fuse.js": "^3.4.6", + "fs-extra": "^9.0.1", + "fuse.js": "^3.6.1", "google-closure-compiler": "^20191111.0.0", "highlight.js": "^9.18.1", "html-minifier": "^3.5.21", - "js-yaml": "^3.13.1", + "js-yaml": "^3.14.0", "markdown-it": "3.1.0", "maxrects-packer": "^2.6.0", "monaco-editor": "^0.20.0", "monaco-themes": "^0.3.3", "node-static": "^0.7.11", - "pixi-particles": "^4.2.0", + "pixi-particles": "^4.2.1", "pixi.js-legacy": "5.1.2", "png2icons": "^2.0.1", - "ttf2woff": "^2.0.1", - "unzipper": "^0.10.10" + "ttf2woff": "^2.0.2", + "unzipper": "^0.10.11" } } diff --git a/package-lock.json b/package-lock.json index df9c56192..a5a3913fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,8 +769,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" } } }, @@ -2708,9 +2707,9 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-glob": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz", - "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2792,9 +2791,9 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fastq": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz", - "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", "requires": { "reusify": "^1.0.4" } @@ -3305,9 +3304,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", - "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "requires": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -3316,9 +3315,9 @@ }, "dependencies": { "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "jsonfile": { "version": "6.0.1", @@ -4263,8 +4262,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" }, "micromatch": { "version": "3.1.10", @@ -4327,9 +4325,9 @@ } }, "globby": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", - "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4340,9 +4338,9 @@ }, "dependencies": { "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" } } }, @@ -6927,9 +6925,9 @@ } }, "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { "version": "2.3.11", @@ -8739,8 +8737,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "resolved": "" } } }, diff --git a/package.json b/package.json index 18779ea53..95d2803b9 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "license": "MIT", "dependencies": { "filemode": "^3.0.0", - "fs-extra": "^9.0.0", - "globby": "^11.0.0", + "fs-extra": "^9.0.1", + "globby": "^11.0.1", "gulp": "^4.0.2", "gulp-append-prepend": "^1.0.8", "gulp-chmod": "^3.0.0", From d8bc8002c8a0f3f026a44574f32ae0bcbda64f19 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 02:03:40 +1200 Subject: [PATCH 24/86] :construction: --- src/styl/3rdParty/alertify.styl | 14 ++-- src/styl/buildingBlocks.styl | 66 ++++++++--------- src/styl/common.styl | 16 ++--- src/styl/hvost.styl | 2 +- src/styl/inputs.styl | 72 +++++++++---------- src/styl/tabs.styl | 8 +-- src/styl/tags/debugger/debugger-screen.styl | 2 +- src/styl/tags/debugger/debugger-toolbar.styl | 28 ++++---- src/styl/tags/font-editor.styl | 2 +- src/styl/tags/hello.styl | 4 +- src/styl/tags/new-project-onboarding.styl | 2 +- src/styl/tags/particles/emitter-editor.styl | 6 +- src/styl/tags/project-selector.styl | 4 +- src/styl/tags/rooms/room-editor.styl | 8 +-- src/styl/tags/rooms/rooms-panel.styl | 2 +- .../tags/settings/actions-input-selector.styl | 2 +- src/styl/tags/settings/script-editor.styl | 2 +- src/styl/tags/settings/scripts-panel.styl | 4 +- src/styl/tags/shared/collapsible-section.styl | 4 +- src/styl/tags/shared/color-input.styl | 2 +- src/styl/tags/shared/color-picker.styl | 16 ++--- src/styl/tags/shared/context-menu.styl | 18 ++--- src/styl/tags/shared/curve-editor.styl | 16 ++--- src/styl/tags/sounds/sound-editor.styl | 2 +- src/styl/tags/textures/texture-editor.styl | 2 +- src/styl/tags/types/type-editor.styl | 2 +- src/styl/themeDay.styl | 20 +++--- src/styl/themeHorizon.styl | 20 +++--- src/styl/themeLucasDracula.styl | 64 +++++++++++++++++ src/styl/themeNight.styl | 22 +++--- src/styl/themeSpringStream.styl | 26 +++---- src/styl/typography.styl | 10 +-- 32 files changed, 268 insertions(+), 200 deletions(-) create mode 100644 src/styl/themeLucasDracula.styl diff --git a/src/styl/3rdParty/alertify.styl b/src/styl/3rdParty/alertify.styl index dab880bfa..3cbd52209 100644 --- a/src/styl/3rdParty/alertify.styl +++ b/src/styl/3rdParty/alertify.styl @@ -3,7 +3,7 @@ {bs} border-radius 1px &.default - border 1px solid bd + border 1px solid borderBright &.error border 1px solid red &.success @@ -12,7 +12,7 @@ border 1px solid orange .alertify-logs > *, .alertify-logs > *.default - background white + background background .alertify position fixed background-color rgba(0, 0, 0, 0.3) @@ -44,9 +44,9 @@ max-width 95% margin 0 auto padding 1rem 1.5rem - background white + background background border-radius br - border 1px solid bd + border 1px solid borderBright {shadamb} .msg margin 0 @@ -56,15 +56,15 @@ outline 0 display inline-block align-items center - white-space nowrap + background-space nowrap text-align center padding 0.2em 0.8em nav margin 1.5rem -1.5rem -1rem padding 1rem text-align right - background bl - border-top 1px solid bd + background borderPale + border-top 1px solid borderBright border-radius 0 0 br br .alertify-logs position fixed diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index 6b3fbb3f5..b75268b94 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -15,12 +15,12 @@ .modal, .panel box-sizing border-box - background white + background background if (themeDark) - background bl - border 1px solid bl + background borderPale + border 1px solid borderPale if (themeDark) - border 1px solid bd + border 1px solid borderBright border-radius br position relative &.pad @@ -39,9 +39,9 @@ .view overflow auto - background snow + background backgroundDeeper if (themeDark) - background white + background background position absolute left 0 right 0 @@ -52,10 +52,10 @@ border 0 .inset - background bl + background borderPale padding 1rem margin 0 -1rem - border-top 1px solid bd + border-top 1px solid borderBright .aStrippedList margin 0.5rem 0 @@ -64,9 +64,9 @@ list-style none padding 0.2em 0.8em margin 0 - border-bottom 1px solid bl + border-bottom 1px solid borderPale if (themeDark) - border-bottom 1px solid bd + border-bottom 1px solid borderBright // Меню выбора - список @@ -78,26 +78,26 @@ cursor pointer overflow hidden text-overflow ellipsis - white-space nowrap + background-space nowrap {trans} &:hover, &.hover {transshort} - border-bottom-color bd + border-bottom-color borderBright color acttext padding 0.2em 0.5em 0.2em 1.1em &:active, &.active background act border-bottom-color transparent - color white + color background // панели управления .nav - background white + background background if (themeDark) - background bl + background borderPale border-radius br - border-top 1px solid bd - border-left 1px solid bd + border-top 1px solid borderBright + border-left 1px solid borderBright &.vertical border 0 border-radius 0 @@ -110,16 +110,16 @@ li text-align center cursor pointer - border-right 1px solid bd - border-bottom 1px solid bd + border-right 1px solid borderBright + border-bottom 1px solid borderBright flex auto list-style none padding 0.25em margin 0 {trans} - box-shadow 0 0 white inset + box-shadow 0 0 background inset if (themeDark) - background bl + background borderPale &:hover, &.active {transshort} color acttext @@ -129,10 +129,10 @@ box-shadow 0 -2px accent1 inset &:active background acttext - color white + color background box-shadow 0 -2px acttext inset & > svg - color white + color background &:first-child border-top-left-radius inherit border-bottom-left-radius inherit @@ -145,7 +145,7 @@ padding 0.25rem 1rem overflow hidden text-overflow ellipsis - white-space nowrap + background-space nowrap &:hover, &.active box-shadow -2px 0 accent1 inset &:active @@ -161,11 +161,11 @@ grid-gap 0.3em flex-grow 0 li - background white + background background padding 0.8em margin 0 box-sizing border-box - border 1px solid bl + border 1px solid borderPale border-radius 3px display inline-block vertical-align top @@ -176,13 +176,13 @@ &:active border-color accent1 background act - color white + color background span font-family consolas, monospace width calc(14.5em - 64px) display inline-block text-overflow ellipsis - white-space nowrap + background-space nowrap overflow hidden > .date display none @@ -205,7 +205,7 @@ width auto &:hover background act - color white + color background span flex 1 1 auto order 4 @@ -239,7 +239,7 @@ sounds-panel, rooms-panel background none .anErrorNotice - background mix(error, white, 10%) + background mix(error, background, 10%) border 1px solid error border-radius br padding 0.5rem 1rem @@ -250,7 +250,7 @@ sounds-panel, rooms-panel z-index 100 transform-origin 50% -0.5rem &::before - background mix(error, white, 10%) + background mix(error, background, 10%) content '' left 50% top -0.33rem @@ -263,7 +263,7 @@ sounds-panel, rooms-panel .nicetable, #moduleinfo table, #modulesettings table, #modulehelp table margin 1rem 0 - border 1px solid bd + border 1px solid borderBright border-radius br border-spacing 0 {shadamb} @@ -271,5 +271,5 @@ sounds-panel, rooms-panel padding 0.5rem 1.5rem margin 0 th - border-bottom 1px solid bd + border-bottom 1px solid borderBright text-align left \ No newline at end of file diff --git a/src/styl/common.styl b/src/styl/common.styl index 25683e2c1..b9e186b8e 100644 --- a/src/styl/common.styl +++ b/src/styl/common.styl @@ -9,7 +9,7 @@ html, body body font 16px/32px fonts color text - background white + background background cursor default overflow hidden @@ -31,15 +31,15 @@ body position absolute .borderall - border 1px solid bd + border 1px solid borderBright .borderleft - border-left 1px solid bd + border-left 1px solid borderBright .borderright - border-right 1px solid bd + border-right 1px solid borderBright .bordertop - border-top 1px solid bd + border-top 1px solid borderBright .borderbottom - border-bottom 1px solid bd + border-bottom 1px solid borderBright .tall height 100% @@ -62,7 +62,7 @@ body min-width 10em position relative border-radius br - border 1px solid bd + border 1px solid borderBright height 1.5em vertical-align middle display inline-block @@ -92,7 +92,7 @@ div#loading line-height 86vh text-align center z-index 50 - background white + background background font-size 4vw font-weight 300 diff --git a/src/styl/hvost.styl b/src/styl/hvost.styl index 9d2c10529..be6361073 100644 --- a/src/styl/hvost.styl +++ b/src/styl/hvost.styl @@ -61,7 +61,7 @@ html .crop overflow hidden text-overflow ellipsis - white-space nowrap + background-space nowrap display inline-block max-width 15em vertical-align text-bottom diff --git a/src/styl/inputs.styl b/src/styl/inputs.styl index 5bd51a080..fa11f966a 100644 --- a/src/styl/inputs.styl +++ b/src/styl/inputs.styl @@ -24,16 +24,16 @@ input[type="password"], select, textarea padding 0.1em 0.8em - background white - border 1px solid bl + background background + border 1px solid borderPale if (themeDark) - border 1px solid bd + border 1px solid borderBright border-radius br font inherit color inherit {trans} &:hover - border-color bd + border-color borderBright {transshort} &:focus border-color acttext @@ -59,7 +59,7 @@ input[type="reset"], font inherit font-weight 400 border-radius br - background white + background background if (themeDark) background transparent display inline-block @@ -68,29 +68,29 @@ input[type="reset"], {trans} &:hover, &.active background acttext - color white + color background {transshort} if (themeDark) - color black + color foreground if (!themeDark) border-color accent1 svg - color white + color background if (themeDark) - color black + color foreground svg - color black + color foreground &:active, &.selected {transshort} border-color accent1 background accent1 - color white + color background svg - color white + color background if (themeDark) - color black + color foreground svg - color black + color foreground &.inline padding 0.2em 0.8em display inline-block @@ -127,9 +127,9 @@ siz = 4em width siz height siz border-radius 50% 10% 50% 50% - background -webkit-linear-gradient(top, white 0, bl 100%) - box-shadow 0 0 0.2em snow inset - border 1px solid bd + background -webkit-linear-gradient(top, background 0, borderPale 100%) + box-shadow 0 0 0.2em backgroundDeeper inset + border 1px solid borderBright if (themeDark) border-color act text-align center @@ -154,7 +154,7 @@ siz = 4em border-radius 100% border 1px solid box-sizing border-box - background -webkit-linear-gradient(top, bl 0, white 100%) + background -webkit-linear-gradient(top, borderPale 0, background 100%) color success span position absolute @@ -230,9 +230,9 @@ select left -1px // why? right -1px top 100% - background white + background background color text - border 1px solid bd + border 1px solid borderBright border-radius 0 0 br br border-bottom 0 border-top 1px solid act @@ -285,14 +285,14 @@ fieldset {trans} &::after content '' - icon(check, white) + icon(check, background) width 1rem height 1rem position absolute top -0.025rem left -0.05rem line-height 1 - color white + color background opacity 0 {trans} &:checked @@ -327,26 +327,26 @@ input[type="range"] width 100% height 24px cursor pointer - background snow + background backgroundDeeper if (themeDark) // dark themes use an inverted color scheme in sense of variables - background white + background background border-radius br - border 1px solid bl + border 1px solid borderPale if (themeDark) - border 1px solid bd + border 1px solid borderBright &::-webkit-slider-thumb - border 1px solid bd + border 1px solid borderBright height 40px width 19px border-radius br - background white + background background if (themeDark) - background snow + background backgroundDeeper cursor pointer -webkit-appearance none margin-top -9px &:focus::-webkit-slider-runnable-track - background white + background background &.transparent &::-webkit-slider-runnable-track, &:focus::-webkit-slider-runnable-track background transparent @@ -376,7 +376,7 @@ input[type="range"] cursor move {shad} &.selected - box-shadow 0 0 0 2px bl, 0 0 0 4px act, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) + box-shadow 0 0 0 2px borderPale, 0 0 0 4px act, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) .aClicker height 0.75rem @@ -393,7 +393,7 @@ input[type="range"] right 0 top 0 bottom 0 - background rgba(white, 0.65) + background rgba(background, 0.65) // @stylint off background-image: linear-gradient(to right, accent1 0, accent1 35%, transparent 35%, transparent 100%), @@ -420,11 +420,11 @@ input[type="range"] cursor -webkit-grabbing .aResizer - background snow + background backgroundDeeper flex 0 0 auto width 0.75rem height 0.75rem - border 1px solid bd + border 1px solid borderBright position relative cursor move &::before @@ -432,9 +432,9 @@ input[type="range"] width 1.5rem height 2px box-sizing content-box - border 1px solid bl + border 1px solid borderPale if (themeDark) - border 1px solid bd + border 1px solid borderBright border-left 0 border-right 0 position absolute diff --git a/src/styl/tabs.styl b/src/styl/tabs.styl index dafcb9b36..86bc97463 100644 --- a/src/styl/tabs.styl +++ b/src/styl/tabs.styl @@ -1,13 +1,13 @@ // табы .tabbed border-radius 0 0 br br - border 1px solid bl + border 1px solid borderPale border-top 0 none padding 1em - background white + background background if (themeDark) - background bl - border 1px solid bd + background borderPale + border 1px solid borderBright overflow auto .tabbed, .tall diff --git a/src/styl/tags/debugger/debugger-screen.styl b/src/styl/tags/debugger/debugger-screen.styl index 95cb225c8..d462f69ad 100644 --- a/src/styl/tags/debugger/debugger-screen.styl +++ b/src/styl/tags/debugger/debugger-screen.styl @@ -2,7 +2,7 @@ display flex flex-flow row nowrap padding 0 0.5rem - border-top 1px solid bd + border-top 1px solid borderBright .debugger-toolbar-aButton padding 0.15rem 0.5rem flex 1 1 auto diff --git a/src/styl/tags/debugger/debugger-toolbar.styl b/src/styl/tags/debugger/debugger-toolbar.styl index f4489d126..5dfa9a6ba 100644 --- a/src/styl/tags/debugger/debugger-toolbar.styl +++ b/src/styl/tags/debugger/debugger-toolbar.styl @@ -10,8 +10,8 @@ debugger-toolbar width 100% height 100% border-radius br - border 1px solid bd - background white + border 1px solid borderBright + background background display flex box-sizing border-box align-items stretch @@ -22,46 +22,46 @@ debugger-toolbar align-content stretch padding 0 0.5rem .&-aDragger - color bd + color borderBright cursor move -webkit-app-region drag app-region drag .&-aButton color act cursor pointer - background white + background background if (themeDark) background transparent {trans} &:hover, &.active background acttext - color white + color background {transshort} if (themeDark) - color black + color foreground if (!themeDark) border-color accent1 svg - color white + color background if (themeDark) - color black + color foreground svg - color black + color foreground &:active, &.selected {transshort} border-color accent1 background accent1 - color white + color background svg - color white + color background if (themeDark) - color black + color foreground svg - color black + color foreground &:last-child border-radius 0 br br 0 .&-aDivider - border-right 1px solid bd + border-right 1px solid borderBright height 100% width 0 margin 0 0.5rem \ No newline at end of file diff --git a/src/styl/tags/font-editor.styl b/src/styl/tags/font-editor.styl index 7c38b7e34..1c348c6ad 100644 --- a/src/styl/tags/font-editor.styl +++ b/src/styl/tags/font-editor.styl @@ -10,7 +10,7 @@ font-editor p margin 0 0 1rem padding 0.5rem 3rem 0.5rem 1rem - border-top 1px solid bl + border-top 1px solid borderPale position relative &::after content attr(data-size) 'px' diff --git a/src/styl/tags/hello.styl b/src/styl/tags/hello.styl index 3f60b9fcb..ecbb99c80 100644 --- a/src/styl/tags/hello.styl +++ b/src/styl/tags/hello.styl @@ -21,8 +21,8 @@ display inline-block position relative padding 0.35em 0.5em 1em - background snow - border 1px solid bd + background backgroundDeeper + border 1px solid borderBright border-radius br margin 0 0.5em cursor pointer diff --git a/src/styl/tags/new-project-onboarding.styl b/src/styl/tags/new-project-onboarding.styl index 569cad3b5..bb1b260dd 100644 --- a/src/styl/tags/new-project-onboarding.styl +++ b/src/styl/tags/new-project-onboarding.styl @@ -41,7 +41,7 @@ new-project-onboarding &:last-child border-bottom 0 &:active svg - color white + color background margin-bottom 1rem .inset padding 1rem 2rem diff --git a/src/styl/tags/particles/emitter-editor.styl b/src/styl/tags/particles/emitter-editor.styl index ec9317e37..4e63e45ff 100644 --- a/src/styl/tags/particles/emitter-editor.styl +++ b/src/styl/tags/particles/emitter-editor.styl @@ -7,12 +7,12 @@ emitter-editor margin-bottom 0 .emitter-editor-aHeader margin -1rem -1rem 1rem - border-bottom 1px solid bd + border-bottom 1px solid borderBright padding 0.5rem 1rem display flex position sticky top -1rem - background snow + background backgroundDeeper border-radius br br 0 0 z-index 2 display flex @@ -22,7 +22,7 @@ emitter-editor margin 0 flex 1 1 auto overflow hidden - white-space nowrap + background-space nowrap text-overflow ellipsis padding-right 0.5em box-sizing border-box diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index 6e87cc0f7..5d4974265 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -14,7 +14,7 @@ project-selector max-width 95% margin 0 auto 2rem padding 1rem - background rgba(white, 0.9) + background rgba(background, 0.9) flex-flow column nowrap z-index 35 {shadamb} @@ -55,7 +55,7 @@ project-selector height 15em span overflow hidden - white-space nowrap + background-space nowrap width 100% display block text-overflow ellipsis diff --git a/src/styl/tags/rooms/room-editor.styl b/src/styl/tags/rooms/room-editor.styl index b68d3bf64..3b098035a 100644 --- a/src/styl/tags/rooms/room-editor.styl +++ b/src/styl/tags/rooms/room-editor.styl @@ -45,13 +45,13 @@ room-editor left 0.5rem .shift, .center span - text-shadow 0 1px 0 white + text-shadow 0 1px 0 background .zoom position absolute top 0.5rem right 0.5rem b - text-shadow 0 1px 0 white + text-shadow 0 1px 0 background .grid position absolute bottom 0.5rem @@ -76,7 +76,7 @@ room-editor align-items flex-start .room-editor-aTypeSwatch list-style none - border 1px solid bl + border 1px solid borderPale flex 1 0 5rem display inline-block box-sizing border-box @@ -130,7 +130,7 @@ room-editor list-style none padding 0.3em 0.8em border-radius br - border 1px solid bd + border 1px solid borderBright margin-bottom 0.2em img float left diff --git a/src/styl/tags/rooms/rooms-panel.styl b/src/styl/tags/rooms/rooms-panel.styl index 82cb6d1f2..26ec3736c 100644 --- a/src/styl/tags/rooms/rooms-panel.styl +++ b/src/styl/tags/rooms/rooms-panel.styl @@ -18,7 +18,7 @@ rooms-panel font-family consolas, monospace width 15em text-overflow ellipsis - white-space nowrap + background-space nowrap overflow hidden .starting svg position absolute diff --git a/src/styl/tags/settings/actions-input-selector.styl b/src/styl/tags/settings/actions-input-selector.styl index a270958f7..3b03d52cd 100644 --- a/src/styl/tags/settings/actions-input-selector.styl +++ b/src/styl/tags/settings/actions-input-selector.styl @@ -8,5 +8,5 @@ actions-input-selector bottom 5rem .panel padding 1rem - box-shadow 0 1rem 2rem rgba(black, 0.35) + box-shadow 0 1rem 2rem rgba(foreground, 0.35) height 100% \ No newline at end of file diff --git a/src/styl/tags/settings/script-editor.styl b/src/styl/tags/settings/script-editor.styl index b222dc8bb..150ad45f2 100644 --- a/src/styl/tags/settings/script-editor.styl +++ b/src/styl/tags/settings/script-editor.styl @@ -3,7 +3,7 @@ script-editor padding 1rem .aCodeEditor border-radius br - border 1px solid bd + border 1px solid borderBright position absolute left 0 width 100% diff --git a/src/styl/tags/settings/scripts-panel.styl b/src/styl/tags/settings/scripts-panel.styl index 3ec832288..2792f82d0 100644 --- a/src/styl/tags/settings/scripts-panel.styl +++ b/src/styl/tags/settings/scripts-panel.styl @@ -8,7 +8,7 @@ scripts-settings padding-left 0.5rem padding-right 0 .dim - color bd + color borderBright {trans} .scripts-settings-aDeleteButton margin-left 1rem @@ -19,4 +19,4 @@ scripts-settings color red &:active svg - color white \ No newline at end of file + color background \ No newline at end of file diff --git a/src/styl/tags/shared/collapsible-section.styl b/src/styl/tags/shared/collapsible-section.styl index 6704f3fe5..b6c9647a3 100644 --- a/src/styl/tags/shared/collapsible-section.styl +++ b/src/styl/tags/shared/collapsible-section.styl @@ -7,14 +7,14 @@ collapsible-section padding 0.5rem 0 align-items center cursor pointer - border-bottom 1px solid bd + border-bottom 1px solid borderBright h3 padding 0 margin 0 flex 1 1 auto overflow hidden text-overflow ellipsis - white-space nowrap + background-space nowrap svg {trans} margin-left 0.5rem diff --git a/src/styl/tags/shared/color-input.styl b/src/styl/tags/shared/color-input.styl index 4dbcbacef..059dd2944 100644 --- a/src/styl/tags/shared/color-input.styl +++ b/src/styl/tags/shared/color-input.styl @@ -1,7 +1,7 @@ .color-input-aPicker display block cursor pointer - border 1px solid bl + border 1px solid borderPale border-radius br box-sizing border-box max-width 10rem diff --git a/src/styl/tags/shared/color-picker.styl b/src/styl/tags/shared/color-picker.styl index 57ea7c2aa..d536446df 100644 --- a/src/styl/tags/shared/color-picker.styl +++ b/src/styl/tags/shared/color-picker.styl @@ -7,7 +7,7 @@ color-picker top 10rem .panel padding 1rem - box-shadow 0 1rem 2rem rgba(black, 0.35) + box-shadow 0 1rem 2rem rgba(foreground, 0.35) .aRangePipeStack width 15rem margin-right 0.5rem @@ -26,30 +26,30 @@ color-picker .huebar background: linear-gradient(to right, hsl(0, 100%, 50%) 0, hsl(18, 100%, 50%) 5%, hsl(36, 100%, 50%) 10%, hsl(54, 100%, 50%) 15%, hsl(60, 100%, 50%) 16.667%, hsl(72, 100%, 50%) 20%, hsl(90, 100%, 50%) 25%, hsl(108, 100%, 50%) 30%, hsl(108, 100%, 50%) 35%, hsl(144, 100%, 50%) 40%, hsl(162, 100%, 50%) 45%, hsl(180, 100%, 50%) 50%, hsl(198, 100%, 50%) 55%, hsl(216, 100%, 50%) 60%, hsl(240, 100%, 50%) 66%, hsl(257, 100%, 50%) 70%, hsl(270, 100%, 50%) 75%, hsl(288, 100%, 50%) 80%, hsl(300, 100%, 50%) 83.333%, hsl(306, 100%, 50%) 85%, hsl(324, 100%, 50%) 90%, hsl(342, 100%, 50%) 95%, hsl(0, 100%, 50%) 100%) .alphabar - background linear-gradient(to bottom right, white 0, white 24%, text 25%, text 49%, white 50%, white 74%, text 75%, text 99%, white 100%) + background linear-gradient(to bottom right, background 0, background 24%, text 25%, text 49%, background 50%, background 74%, text 75%, text 99%, background 100%) background-size 0.5rem 0.5rem .color-picker-aBackgroundToggler float right - border 0.15rem bl solid + border 0.15rem borderPale solid width 1rem height 1rem box-sizing border-box - background black + background foreground cursor pointer border-radius 100% position absolute right 0.5rem top 0.5rem &:hover - background white + background background .color-picker-aBackgoundWell padding 1rem margin -1rem -1rem 1rem - background white + background background border-radius br :hover + & - background black + background foreground .c6 border-radius br text-align center @@ -63,7 +63,7 @@ color-picker border-radius br display inline-block margin 0 0.5rem 0.5rem 0 - border 1px bd solid + border 1px borderBright solid box-sizing border-box vertical-align middle cursor pointer \ No newline at end of file diff --git a/src/styl/tags/shared/context-menu.styl b/src/styl/tags/shared/context-menu.styl index d35bb1266..a2e8d3a5d 100644 --- a/src/styl/tags/shared/context-menu.styl +++ b/src/styl/tags/shared/context-menu.styl @@ -6,10 +6,10 @@ context-menu visibility hidden {shad} border-radius br - border 1px solid bd - background white + border 1px solid borderBright + background background if (themeDark) - background bl + background borderPale z-index 900 width max-content height max-content @@ -54,10 +54,10 @@ context-menu width 1.125rem height @width &:hover, &:focus - color white + color background background-color act & > svg - color white + color background {transshort} &:active background-color accent1 @@ -66,9 +66,9 @@ context-menu right 0.5rem top 0.5rem position absolute - color bd + color borderBright if (themeDark) - color mix(bd, text, 85%) + color mix(borderBright, text, 85%) a, a.checkbox display block position relative @@ -79,9 +79,9 @@ context-menu [type="checkbox"], [type="radio"] left 1rem top 0.75rem - background white + background background .separator padding 0 margin 0.5rem 0 - border-top 1px solid bd + border-top 1px solid borderBright cursor initial \ No newline at end of file diff --git a/src/styl/tags/shared/curve-editor.styl b/src/styl/tags/shared/curve-editor.styl index 4587006e1..e07825b32 100644 --- a/src/styl/tags/shared/curve-editor.styl +++ b/src/styl/tags/shared/curve-editor.styl @@ -5,25 +5,25 @@ curve-editor cursor default .curve-editor-aGraphWrap position relative - outline solid 1px bd + outline solid 1px borderBright if (themeDark) - outline solid 1px mix(bd, text, 85%) + outline solid 1px mix(borderBright, text, 85%) line-height 0 .aDragger transform translate(-50%, -50%) .safecolors & - box-shadow 0 0 0 2px white, 0 0 0 4px bd, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) + box-shadow 0 0 0 2px background, 0 0 0 4px borderBright, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) &.selected - box-shadow 0 0 0 2px bl, 0 0 0 4px act, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) + box-shadow 0 0 0 2px borderPale, 0 0 0 4px act, 0 0.1rem 0.2rem 4px rgba(0, 0, 0, 0.5) .curve-editor-aGrid - stroke bl + stroke borderPale stroke-width 1px if (themeDark) - stroke bd + stroke borderBright .aMiddleLine - stroke bd + stroke borderBright if (themeDark) - stroke mix(bd, text, 85%) + stroke mix(borderBright, text, 85%) .curve-editor-aCurve stroke act stroke-width 4px diff --git a/src/styl/tags/sounds/sound-editor.styl b/src/styl/tags/sounds/sound-editor.styl index bb1f132d6..4ddd31372 100644 --- a/src/styl/tags/sounds/sound-editor.styl +++ b/src/styl/tags/sounds/sound-editor.styl @@ -3,7 +3,7 @@ sound-editor padding 1em calc(1em + 0.5px) width 21em vertical-align middle - background white + background background color text if (themeDark) audio diff --git a/src/styl/tags/textures/texture-editor.styl b/src/styl/tags/textures/texture-editor.styl index 48d0b62ee..29a63e5d3 100644 --- a/src/styl/tags/textures/texture-editor.styl +++ b/src/styl/tags/textures/texture-editor.styl @@ -34,7 +34,7 @@ texture-editor overflow auto margin -0.5rem -1rem 0.5rem padding 0.5rem - border-bottom 1px solid bl + border-bottom 1px solid borderPale @media (max-height 680px) height 6em diff --git a/src/styl/tags/types/type-editor.styl b/src/styl/tags/types/type-editor.styl index 353908336..7ee45b46c 100644 --- a/src/styl/tags/types/type-editor.styl +++ b/src/styl/tags/types/type-editor.styl @@ -30,7 +30,7 @@ type-editor right 0 bottom 0 if (theme == 'Horizon') - border 1px solid bd + border 1px solid borderBright border-top 0 box-sizing border-box border-radius 0 0 br br diff --git a/src/styl/themeDay.styl b/src/styl/themeDay.styl index 4d8b50edd..70f366332 100644 --- a/src/styl/themeDay.styl +++ b/src/styl/themeDay.styl @@ -1,25 +1,27 @@ @charset "utf-8" -// минимальная ширина шаблона - 380 пикс +background = #fff +foreground = #333 +shadows = #000 -/* частотники */ +/* Frequently used properties */ trans = transition 0.35s ease all transshort = transition 0.15s ease all shad = - box-shadow 0 0.1rem 0.2rem rgba(0, 0, 0, 0.5) + box-shadow 0 0.1rem 0.2rem rgba(shadows, 0.5) shadamb = - box-shadow 0 0 0.35rem rgba(0, 0, 0, 0.5) + box-shadow 0 0 0.35rem rgba(shadows, 0.5) -/* шрифты */ +/* Base fonts for UI */ fonts = font = 'Open Sans', sans-serif, serif font-mono = mono = Iosevka, monospace br = 0.2rem iconsize = 1.5rem -/* цвета */ +/* Colors used by this theme */ act = #446adb acttext = #446adb accent1 = #5144db @@ -41,11 +43,11 @@ themeDark = false .warning color warning -bl = #e1e2e5 -bd = #c8cdd1 +borderPale = #e1e2e5 +borderBright = #c8cdd1 text = #666 -snow = #fafafa +backgroundDeeper = #fafafa @require 'hvost.styl' diff --git a/src/styl/themeHorizon.styl b/src/styl/themeHorizon.styl index 099445d79..e3292aa04 100644 --- a/src/styl/themeHorizon.styl +++ b/src/styl/themeHorizon.styl @@ -1,6 +1,8 @@ @charset "utf-8" -// The minimum width of ct.js = 380px +foreground = #fff +background = #1C1E26 +shadows = #E95378 /* handy blocks */ trans = @@ -8,9 +10,9 @@ trans = transshort = transition 0.15s ease all shad = - box-shadow 0 0.1rem 0.2rem rgba(0, 0, 0, 0.5) + box-shadow 0 0.1rem 0.2rem rgba(shadows, 0.5) shadamb = - box-shadow 0 0 0.35rem rgba(0, 0, 0, 0.5) + box-shadow 0 0 0.35rem rgba(shadows, 0.5) /* Fonts */ fonts = font = 'Open Sans', sans-serif, serif @@ -20,8 +22,6 @@ br = 0.2rem iconsize = 1.5rem /* Basic colors & accents */ -black = #fff -white = #1C1E26 act = #E95378 acttext = act accent1 = #E95378 @@ -45,11 +45,11 @@ themeDark = true .warning color warning -bl = #252732 -bd = #2F3038 +borderPale = #252732 +borderBright = #2F3038 text = #D5D8DA -snow = mix(white, bl, 50%) +backgroundDeeper = mix(background, borderPale, 50%) @require 'hvost.styl' @@ -72,7 +72,7 @@ input[type="reset"], border-width 2px .view - background white + background background .tabbed, .nav border 0 @@ -80,7 +80,7 @@ input[type="reset"], border-right 0 border-left 0 &:hover, &.active - color black + color foreground #moduleinfo .bigpower:not(.off) border-color success diff --git a/src/styl/themeLucasDracula.styl b/src/styl/themeLucasDracula.styl new file mode 100644 index 000000000..f1df2a79a --- /dev/null +++ b/src/styl/themeLucasDracula.styl @@ -0,0 +1,64 @@ +@charset "utf-8" + + +/* Frequently used properties */ +trans = + transition 0.35s ease all +transshort = + transition 0.15s ease all +shad = + box-shadow 0 0.1rem 0.2rem rgba(0, 0, 0, 0.5) +shadamb = + box-shadow 0 0 0.35rem rgba(0, 0, 0, 0.5) + +/* Base fonts for UI */ +fonts = font = 'Open Sans', sans-serif, serif +font-mono = mono = Iosevka, monospace + +br = 0.2rem +iconsize = 1.5rem + +/* Colors used by this theme */ +foreground = #f6f3fc +background = #161526 +act = #FFCFD4 +acttext = act +accent1 = #FF70B1 +accent2 = #FFCFD4 +error = #FF5370 +red = error +success = #2DE4A1 +green = success +warning = #f07178 +orange = warning + +theme = 'LucasDracula' +themeDark = true + +.error + color error +.success + color success +.warning + color warning + +text = #DAD6DA +backgroundDeeper = #282646 + +borderBright = #4D4985 +borderPale = mix(backgroundDeeper, borderBright, 50%) + + +@require 'hvost.styl' + +@require '3rdParty/*.styl' +@require './../../app/node_modules/highlight.js/styles/atom-one-dark.css' + +@require 'common.styl' +@require 'inputs.styl' +@require 'typography.styl' +@require 'confetti.styl' +@require 'buildingBlocks.styl' +@require 'tabs.styl' + +@require 'tags/**/*.styl' \ No newline at end of file diff --git a/src/styl/themeNight.styl b/src/styl/themeNight.styl index 253f8e456..650d47ec9 100644 --- a/src/styl/themeNight.styl +++ b/src/styl/themeNight.styl @@ -1,27 +1,27 @@ @charset "utf-8" -// минимальная ширина шаблона - 380 пикс +foreground = #fff +background = #08080D +shadows = #000 -/* частотники */ +/* Frequently used properties */ trans = transition 0.35s ease all transshort = transition 0.15s ease all shad = - box-shadow 0 0.1rem 0.2rem rgba(0, 0, 0, 0.5) + box-shadow 0 0.1rem 0.2rem rgba(shadows, 0.5) shadamb = - box-shadow 0 0 0.35rem rgba(0, 0, 0, 0.5) + box-shadow 0 0 0.35rem rgba(shadows, 0.5) -/* шрифты */ +/* Base fonts for UI */ fonts = font = 'Open Sans', sans-serif, serif font-mono = mono = Iosevka, monospace br = 0.2rem iconsize = 1.5rem -/* цвета */ -black = #fff -white = #08080D +/* Colors used by this theme */ act = #44dbb5 acttext = act accent1 = #44dbb5 @@ -43,11 +43,11 @@ themeDark = true .warning color warning -bl = #121822 -bd = #1C2B42 +borderPale = #121822 +borderBright = #1C2B42 text = #C8CECD -snow = mix(white, bl, 50%) +backgroundDeeper = mix(background, borderPale, 50%) @require 'hvost.styl' diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index be25d5c16..e0bb774a4 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -1,18 +1,20 @@ @charset "utf-8" -// минимальная ширина шаблона - 380 пикс +background = #fff +foreground = #555 +shadows = #000 -/* частотники */ +/* Frequently used properties */ trans = transition 0.35s ease all transshort = transition 0.15s ease all shad = - box-shadow 0 0.1rem 0.35rem rgba(0, 0, 0, 0.25) + box-shadow 0 0.1rem 0.35rem rgba(shadows, 0.25) shadamb = - box-shadow 0 0.25rem 1rem rgba(0, 0, 0, 0.15) + box-shadow 0 0.25rem 1rem rgba(shadows, 0.15) -/* шрифты */ +/* Base fonts for UI */ fonts = font = 'Open Sans', sans-serif, serif fontsHeaders = fontHeader = Comfortaa, 'Open Sans', sans-serif, serif font-mono = mono = Iosevka, monospace @@ -20,7 +22,7 @@ font-mono = mono = Iosevka, monospace br = 0.35rem iconsize = 1.5rem -/* цвета */ +/* Colors used by this theme */ act = #00c09e acttext = #009170 accent1 = #00c09e @@ -42,11 +44,11 @@ themeDark = false .warning color warning -bl = #d6dedd -bd = #d6dedd +borderPale = #d6dedd +borderBright = #d6dedd text = #555 -snow = #fafafa +backgroundDeeper = #fafafa introBg = #f2fcfa @@ -84,9 +86,9 @@ input[type="reset"], font-weight 900 border-width 0 background accent1 - color white + color background svg - color white + color background &.selected background acttext @@ -99,4 +101,4 @@ input[type="reset"], gap 0.75rem li {shad} - border-color white \ No newline at end of file + border-color background \ No newline at end of file diff --git a/src/styl/typography.styl b/src/styl/typography.styl index 85313e812..655d90b33 100644 --- a/src/styl/typography.styl +++ b/src/styl/typography.styl @@ -18,8 +18,8 @@ dt font-weight 500 pre - background snow - border 1px solid bd + background backgroundDeeper + border 1px solid borderBright border-radius br padding 0.4em 0.8em font-family font-mono @@ -31,9 +31,9 @@ code padding 1.5px 0.5rem margin 0 0.25rem border-radius br - background bl - border 1px solid bd - text-shadow 0 1px 0 rgba(white, 0.65) + background borderPale + border 1px solid borderBright + text-shadow 0 1px 0 rgba(background, 0.65) .active & color act From aaf980b37d29a1f5c32072c483c069ca787cd7ee Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 17:04:02 +1200 Subject: [PATCH 25/86] :bug: Fix background-space :D --- src/styl/3rdParty/alertify.styl | 2 +- src/styl/buildingBlocks.styl | 6 +++--- src/styl/hvost.styl | 2 +- src/styl/tags/particles/emitter-editor.styl | 2 +- src/styl/tags/project-selector.styl | 2 +- src/styl/tags/rooms/rooms-panel.styl | 2 +- src/styl/tags/shared/collapsible-section.styl | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/styl/3rdParty/alertify.styl b/src/styl/3rdParty/alertify.styl index 3cbd52209..c3120dc1b 100644 --- a/src/styl/3rdParty/alertify.styl +++ b/src/styl/3rdParty/alertify.styl @@ -56,7 +56,7 @@ outline 0 display inline-block align-items center - background-space nowrap + white-space nowrap text-align center padding 0.2em 0.8em nav diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index b75268b94..f43eb55b8 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -78,7 +78,7 @@ cursor pointer overflow hidden text-overflow ellipsis - background-space nowrap + white-space nowrap {trans} &:hover, &.hover {transshort} @@ -145,7 +145,7 @@ padding 0.25rem 1rem overflow hidden text-overflow ellipsis - background-space nowrap + white-space nowrap &:hover, &.active box-shadow -2px 0 accent1 inset &:active @@ -182,7 +182,7 @@ width calc(14.5em - 64px) display inline-block text-overflow ellipsis - background-space nowrap + white-space nowrap overflow hidden > .date display none diff --git a/src/styl/hvost.styl b/src/styl/hvost.styl index be6361073..9d2c10529 100644 --- a/src/styl/hvost.styl +++ b/src/styl/hvost.styl @@ -61,7 +61,7 @@ html .crop overflow hidden text-overflow ellipsis - background-space nowrap + white-space nowrap display inline-block max-width 15em vertical-align text-bottom diff --git a/src/styl/tags/particles/emitter-editor.styl b/src/styl/tags/particles/emitter-editor.styl index 4e63e45ff..c8dafe831 100644 --- a/src/styl/tags/particles/emitter-editor.styl +++ b/src/styl/tags/particles/emitter-editor.styl @@ -22,7 +22,7 @@ emitter-editor margin 0 flex 1 1 auto overflow hidden - background-space nowrap + white-space nowrap text-overflow ellipsis padding-right 0.5em box-sizing border-box diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index 5d4974265..6d7ea16e3 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -55,7 +55,7 @@ project-selector height 15em span overflow hidden - background-space nowrap + white-space nowrap width 100% display block text-overflow ellipsis diff --git a/src/styl/tags/rooms/rooms-panel.styl b/src/styl/tags/rooms/rooms-panel.styl index 26ec3736c..82cb6d1f2 100644 --- a/src/styl/tags/rooms/rooms-panel.styl +++ b/src/styl/tags/rooms/rooms-panel.styl @@ -18,7 +18,7 @@ rooms-panel font-family consolas, monospace width 15em text-overflow ellipsis - background-space nowrap + white-space nowrap overflow hidden .starting svg position absolute diff --git a/src/styl/tags/shared/collapsible-section.styl b/src/styl/tags/shared/collapsible-section.styl index b6c9647a3..5652ce290 100644 --- a/src/styl/tags/shared/collapsible-section.styl +++ b/src/styl/tags/shared/collapsible-section.styl @@ -14,7 +14,7 @@ collapsible-section flex 1 1 auto overflow hidden text-overflow ellipsis - background-space nowrap + white-space nowrap svg {trans} margin-left 0.5rem From 855ba5b3373be2f6eb96e8e102d420c355af1c5b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 17:30:01 +1200 Subject: [PATCH 26/86] :zap: More logical color hierarchy --- src/riotTags/particles/emitter-editor.tag | 2 +- .../particles/emitter-tandem-editor.tag | 2 +- src/styl/3rdParty/alertify.styl | 2 +- src/styl/buildingBlocks.styl | 21 +++++-------------- src/styl/inputs.styl | 4 ++-- src/styl/tabs.styl | 5 +---- .../tags/particles/emitter-tandem-editor.styl | 4 +++- src/styl/tags/project-selector.styl | 2 ++ src/styl/tags/shared/context-menu.styl | 2 -- src/styl/themeNight.styl | 14 ++++++------- 10 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/riotTags/particles/emitter-editor.tag b/src/riotTags/particles/emitter-editor.tag index bc2b8e75b..9b25d5ea7 100644 --- a/src/riotTags/particles/emitter-editor.tag +++ b/src/riotTags/particles/emitter-editor.tag @@ -1,4 +1,4 @@ -emitter-editor.panel.pad +emitter-editor.panel.pad.nb .emitter-editor-aHeader img.emitter-editor-aTexture(src="{getPreview()}") h3 {voc.emitterHeading} {opts.emitter.uid.split('-').pop()} diff --git a/src/riotTags/particles/emitter-tandem-editor.tag b/src/riotTags/particles/emitter-tandem-editor.tag index ec0b21d85..a816ee970 100644 --- a/src/riotTags/particles/emitter-tandem-editor.tag +++ b/src/riotTags/particles/emitter-tandem-editor.tag @@ -1,7 +1,7 @@ emitter-tandem-editor.panel.view.flexrow .flexfix(style="width: {panelWidth}px") .flexfix-header - .panel.pad + .panel.pad.nbt.nbl.nbr b {vocGlob.name} br input.wide(type="text" value="{tandem.name}" onchange="{wire('this.tandem.name')}") diff --git a/src/styl/3rdParty/alertify.styl b/src/styl/3rdParty/alertify.styl index c3120dc1b..69db8ed3c 100644 --- a/src/styl/3rdParty/alertify.styl +++ b/src/styl/3rdParty/alertify.styl @@ -63,7 +63,7 @@ margin 1.5rem -1.5rem -1rem padding 1rem text-align right - background borderPale + background backgroundDeeper border-top 1px solid borderBright border-radius 0 0 br br .alertify-logs diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index f43eb55b8..861b22e47 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -16,11 +16,7 @@ .modal, .panel box-sizing border-box background background - if (themeDark) - background borderPale - border 1px solid borderPale - if (themeDark) - border 1px solid borderBright + border 1px solid borderBright border-radius br position relative &.pad @@ -40,8 +36,6 @@ .view overflow auto background backgroundDeeper - if (themeDark) - background background position absolute left 0 right 0 @@ -52,10 +46,10 @@ border 0 .inset - background borderPale + background backgroundDeeper padding 1rem margin 0 -1rem - border-top 1px solid borderBright + border-top 1px solid borderPale .aStrippedList margin 0.5rem 0 @@ -65,8 +59,6 @@ padding 0.2em 0.8em margin 0 border-bottom 1px solid borderPale - if (themeDark) - border-bottom 1px solid borderBright // Меню выбора - список @@ -93,8 +85,6 @@ // панели управления .nav background background - if (themeDark) - background borderPale border-radius br border-top 1px solid borderBright border-left 1px solid borderBright @@ -110,7 +100,7 @@ li text-align center cursor pointer - border-right 1px solid borderBright + border-right 1px solid borderPale border-bottom 1px solid borderBright flex auto list-style none @@ -118,8 +108,6 @@ margin 0 {trans} box-shadow 0 0 background inset - if (themeDark) - background borderPale &:hover, &.active {transshort} color acttext @@ -142,6 +130,7 @@ &.vertical li text-align initial border-right 0 + border-bottom 1px solid borderPale padding 0.25rem 1rem overflow hidden text-overflow ellipsis diff --git a/src/styl/inputs.styl b/src/styl/inputs.styl index fa11f966a..d4c3596bb 100644 --- a/src/styl/inputs.styl +++ b/src/styl/inputs.styl @@ -26,8 +26,6 @@ textarea padding 0.1em 0.8em background background border 1px solid borderPale - if (themeDark) - border 1px solid borderBright border-radius br font inherit color inherit @@ -421,6 +419,8 @@ input[type="range"] .aResizer background backgroundDeeper + if (darkTheme) + background background // should be greyish on all themes flex 0 0 auto width 0.75rem height 0.75rem diff --git a/src/styl/tabs.styl b/src/styl/tabs.styl index 86bc97463..5f554b789 100644 --- a/src/styl/tabs.styl +++ b/src/styl/tabs.styl @@ -1,13 +1,10 @@ // табы .tabbed border-radius 0 0 br br - border 1px solid borderPale + border 1px solid borderBright border-top 0 none padding 1em background background - if (themeDark) - background borderPale - border 1px solid borderBright overflow auto .tabbed, .tall diff --git a/src/styl/tags/particles/emitter-tandem-editor.styl b/src/styl/tags/particles/emitter-tandem-editor.styl index 72e47efb8..7c9cd7345 100644 --- a/src/styl/tags/particles/emitter-tandem-editor.styl +++ b/src/styl/tags/particles/emitter-tandem-editor.styl @@ -18,4 +18,6 @@ emitter-tandem-editor right 0 .zoom right 0 - top 0 \ No newline at end of file + top 0 + .panel + border-radius 0 \ No newline at end of file diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index 6d7ea16e3..c391e7443 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -15,6 +15,8 @@ project-selector margin 0 auto 2rem padding 1rem background rgba(background, 0.9) + if (themeDark) + background background flex-flow column nowrap z-index 35 {shadamb} diff --git a/src/styl/tags/shared/context-menu.styl b/src/styl/tags/shared/context-menu.styl index a2e8d3a5d..b8ba1a90a 100644 --- a/src/styl/tags/shared/context-menu.styl +++ b/src/styl/tags/shared/context-menu.styl @@ -8,8 +8,6 @@ context-menu border-radius br border 1px solid borderBright background background - if (themeDark) - background borderPale z-index 900 width max-content height max-content diff --git a/src/styl/themeNight.styl b/src/styl/themeNight.styl index 650d47ec9..07bb948e0 100644 --- a/src/styl/themeNight.styl +++ b/src/styl/themeNight.styl @@ -1,9 +1,15 @@ @charset "utf-8" foreground = #fff -background = #08080D +text = #C8CECD +background = #0c0d17 +backgroundDeeper = #07060E shadows = #000 +borderPale = #142035 +borderBright = #213548 + + /* Frequently used properties */ trans = transition 0.35s ease all @@ -43,12 +49,6 @@ themeDark = true .warning color warning -borderPale = #121822 -borderBright = #1C2B42 - -text = #C8CECD -backgroundDeeper = mix(background, borderPale, 50%) - @require 'hvost.styl' @require '3rdParty/*.styl' From b1e3dc41b39f53a0eb6dc1948ed056af36301278 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 17:40:36 +1200 Subject: [PATCH 27/86] :zap: Better type-editor layout --- src/riotTags/type-editor.tag | 45 ++++++++++++++-------------- src/styl/tags/types/type-editor.styl | 11 ++++++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 769066209..91d6d3307 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -1,26 +1,27 @@ type-editor.panel.view.flexrow - .c3.tall.flexfix - .flexfix-header - #typetexture.panel(onclick="{changeSprite}") - img.ohchangeme(src="{type.texture === -1? 'data/img/notexture.png' : (glob.texturemap[type.texture].src.split('?')[0] + '_prev@2.png?' + getTypeTextureRevision(type)) + getTypeTextureRevision(type)}") - div {voc.change} - b {voc.name} - input#typename.wide(type="text" onchange="{wire('this.type.name')}" value="{type.name}") - .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} - br - b {voc.depth} - input#typedepth.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") - .flexfix-body - extensions-editor(type="type" entity="{type.extends}") - br - br - docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") - .flexfix-footer - button#typedone.wide(onclick="{typeSave}" title="Shift+Control+S" data-hotkey="Control+S") - svg.feather - use(xlink:href="data/icons.svg#check") - span {voc.done} - .c9.tall.borderleft + .type-editor-Properties + .tall.flexfix.panel.pad + .flexfix-header + .type-editor-aTexturePicker.panel(onclick="{changeSprite}") + img.ohchangeme(src="{type.texture === -1? 'data/img/notexture.png' : (glob.texturemap[type.texture].src.split('?')[0] + '_prev@2.png?' + getTypeTextureRevision(type)) + getTypeTextureRevision(type)}") + div {voc.change} + b {voc.name} + input.wide(type="text" onchange="{wire('this.type.name')}" value="{type.name}") + .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} + br + b {voc.depth} + input.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") + .flexfix-body + extensions-editor(type="type" entity="{type.extends}") + br + br + docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") + .flexfix-footer + button#typedone.wide(onclick="{typeSave}" title="Shift+Control+S" data-hotkey="Control+S") + svg.feather + use(xlink:href="data/icons.svg#check") + span {voc.done} + .type-editor-aCodeEditor .tabwrap.tall(style="position: relative") ul.tabs.nav.nogrow.noshrink li(onclick="{changeTab('typeoncreate')}" class="{active: tab === 'typeoncreate'}" title="{voc.create} (Control+Q)" data-hotkey="Control+q") diff --git a/src/styl/tags/types/type-editor.styl b/src/styl/tags/types/type-editor.styl index 7ee45b46c..afc345bc7 100644 --- a/src/styl/tags/types/type-editor.styl +++ b/src/styl/tags/types/type-editor.styl @@ -36,14 +36,23 @@ type-editor border-radius 0 0 br br .tabwrap height 100% + display flex + flex-flow row nowrap + padding 1rem + .type-editor-Properties + flex 0 0 19rem + margin-right 1rem + .type-editor-aCodeEditor + flex 1 1 auto -#typetexture +.type-editor-aTexturePicker width 130px height 130px margin 0.5em auto position relative cursor pointer {trans} + background backgroundDeeper div position absolute left 0 From 100f2a7579810222410c5c2811ec32ed07aa9721 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 19:39:59 +1200 Subject: [PATCH 28/86] :zap: Improve Horizon theme --- src/styl/themeHorizon.styl | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/styl/themeHorizon.styl b/src/styl/themeHorizon.styl index e3292aa04..aea32ab35 100644 --- a/src/styl/themeHorizon.styl +++ b/src/styl/themeHorizon.styl @@ -1,24 +1,29 @@ @charset "utf-8" foreground = #fff +text = #D5D8DA background = #1C1E26 +backgroundDeeper = #1C1E26 shadows = #E95378 +borderPale = #2F3038 +borderBright = #2F3038 + /* handy blocks */ trans = transition 0.35s ease all transshort = transition 0.15s ease all shad = - box-shadow 0 0.1rem 0.2rem rgba(shadows, 0.5) + box-shadow 0 0.15rem 0.35rem rgba(shadows, 0.35) shadamb = - box-shadow 0 0 0.35rem rgba(shadows, 0.5) + box-shadow 0 0.25rem 1.5rem -0.3rem rgba(shadows, 0.5) /* Fonts */ fonts = font = 'Open Sans', sans-serif, serif font-mono = mono = Iosevka, monospace -br = 0.2rem +br = 0.25rem iconsize = 1.5rem /* Basic colors & accents */ @@ -45,11 +50,6 @@ themeDark = true .warning color warning -borderPale = #252732 -borderBright = #2F3038 - -text = #D5D8DA -backgroundDeeper = mix(background, borderPale, 50%) @require 'hvost.styl' @@ -74,8 +74,6 @@ input[type="reset"], .view background background -.tabbed, .nav - border 0 .nav li border-right 0 border-left 0 @@ -87,4 +85,10 @@ input[type="reset"], &:after background success -@require 'tags/**/*.styl' \ No newline at end of file +@require 'tags/**/*.styl' + +gradCore = #733041 +gradTransition = #2e2233 +gradOuter = #1c1e26 +#bg + background radial-gradient(ellipse at bottom, gradCore 0%, gradTransition 50%, gradOuter 100%) \ No newline at end of file From 072a55bacc958e852a7c2586432456e45e8d3620 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 19:40:14 +1200 Subject: [PATCH 29/86] :zap: Minor UI fixes for project selector --- src/styl/tags/project-selector.styl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index c391e7443..03b32bb8c 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -22,6 +22,7 @@ project-selector {shadamb} .inset margin 0 -1rem -1rem + border-radius 0 0 br br .menu max-height 15rem li From 0509de3824c7a773e133da2e7f7c122eea0fd0cd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 19:40:38 +1200 Subject: [PATCH 30/86] :zap: Better project selector background for the night theme --- src/styl/themeNight.styl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/styl/themeNight.styl b/src/styl/themeNight.styl index 07bb948e0..17c5a0b51 100644 --- a/src/styl/themeNight.styl +++ b/src/styl/themeNight.styl @@ -61,4 +61,13 @@ themeDark = true @require 'buildingBlocks.styl' @require 'tabs.styl' -@require 'tags/**/*.styl' \ No newline at end of file +@require 'tags/**/*.styl' + +grad1 = #079880 +grad2 = #30918b +grad3 = #286774 +grad4 = #1a3541 +grad5 = #0C0D18 + +#bg + background radial-gradient(ellipse at bottom, grad1 0%, grad2 9%, grad3 22%, grad4 43%, grad5 74%) \ No newline at end of file From 98d88610483dcd4650dffc6269cb2fe1d486ff34 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 5 Jul 2020 16:31:39 +1200 Subject: [PATCH 31/86] :zap: The left button group at the topmost tab bar now occupies less space on wider screens (cherry picked from commit e333a4de4d82e68ee34f58e492aba6af3999f1e6) --- src/styl/tags/main-menu.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styl/tags/main-menu.styl b/src/styl/tags/main-menu.styl index 697ff67b1..4dca4c4c2 100644 --- a/src/styl/tags/main-menu.styl +++ b/src/styl/tags/main-menu.styl @@ -14,7 +14,7 @@ main-menu #fullscreen flex 0 0 3rem #app - flex 1 1 12rem + flex 0.35 1 12rem #mainnav flex 1 1 60rem From 4f4c583730c44a748969987a516fe9ff610d520b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 19:52:40 +1200 Subject: [PATCH 32/86] :zap: Improve outer project settings' layout --- .../project-settings/project-settings.tag | 4 ++-- src/styl/buildingBlocks.styl | 2 ++ src/styl/tags/project-selector.styl | 4 +--- src/styl/tags/settings/project-settings.styl | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag index 50fa4f3e1..21f992d08 100644 --- a/src/riotTags/project-settings/project-settings.tag +++ b/src/riotTags/project-settings/project-settings.tag @@ -1,4 +1,4 @@ -project-settings.panel.view +project-settings.panel.view.pad.flexrow - var tabs = ['authoring', 'actions', 'branding', 'rendering', 'scripts']; var iconMap = { @@ -9,7 +9,7 @@ project-settings.panel.view scripts: 'terminal', default: 'settings' }; - aside + aside.nogrow.noshrink ul.nav.tabs.vertical // A bit of Pug sorcery, destroyed by Riot.js syntax // Iterate over an array of sections. Template out Riot syntax inside `these` backticks. diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index 861b22e47..d0927590e 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -139,6 +139,8 @@ box-shadow -2px 0 accent1 inset &:active box-shadow -2px 0 acttext inset + &:last-child + border-bottom 0 // resource cards diff --git a/src/styl/tags/project-selector.styl b/src/styl/tags/project-selector.styl index 03b32bb8c..910c6b359 100644 --- a/src/styl/tags/project-selector.styl +++ b/src/styl/tags/project-selector.styl @@ -14,9 +14,7 @@ project-selector max-width 95% margin 0 auto 2rem padding 1rem - background rgba(background, 0.9) - if (themeDark) - background background + background background flex-flow column nowrap z-index 35 {shadamb} diff --git a/src/styl/tags/settings/project-settings.styl b/src/styl/tags/settings/project-settings.styl index 23f8f0900..e72062a57 100644 --- a/src/styl/tags/settings/project-settings.styl +++ b/src/styl/tags/settings/project-settings.styl @@ -1,20 +1,20 @@ project-settings - display flex - flex-flow row nowrap h1 margin 0.5rem 0 aside, main height 100% overflow auto aside - flex 0 0 auto width 18rem - .tabs li - padding-top 0.35rem !important - padding-bottom @padding-top + .nav.tabs + border 1px solid borderBright + border-right 0 + border-radius br 0 0 br + li + padding-top 0.35rem !important + padding-bottom @padding-top main flex 1 1 auto padding 0.5rem 1.5rem - border-width 0 0 0 1px - border-radius 0 + border-radius 0 br br br @extends .panel \ No newline at end of file From 28d429652a090616ff9c6d3216d518ad41e80d17 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 6 Jul 2020 21:21:27 +1200 Subject: [PATCH 33/86] :sparkles: Lucas Dracula theme A rough port of Arkham theme for VSCode by @lucasmsa --- app/data/i18n/English.json | 1 + src/js/codeEditorHelpers.js | 1 + .../monaco-themes/lucasdracula.json | 122 ++++++++++++++++++ src/pug/index.pug | 2 + src/riotTags/license-panel.tag | 31 +++++ src/riotTags/main-menu.tag | 8 ++ src/styl/themeLucasDracula.styl | 28 ++-- 7 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 src/node_requires/monaco-themes/lucasdracula.json diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 77e2c2444..4c4e90126 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -148,6 +148,7 @@ "themeDay": "Light", "themeNight": "Dark", "themeSpringSream": "Spring Stream", + "themeLucasDracula": "Lucas Dracula", "types": "Types", "zipExport": "Export for web", "zipProject": "Pack project to .zip", diff --git a/src/js/codeEditorHelpers.js b/src/js/codeEditorHelpers.js index 3f4f7f5cb..bfedfb1d0 100644 --- a/src/js/codeEditorHelpers.js +++ b/src/js/codeEditorHelpers.js @@ -227,6 +227,7 @@ Night: 'ambiance', Horizon: 'horizon', SpringStream: 'spring', + LucasDracula: 'lucasdracula', default: 'tomorrow' }; const glob = require('./data/node_requires/glob'); diff --git a/src/node_requires/monaco-themes/lucasdracula.json b/src/node_requires/monaco-themes/lucasdracula.json new file mode 100644 index 000000000..4eab75df3 --- /dev/null +++ b/src/node_requires/monaco-themes/lucasdracula.json @@ -0,0 +1,122 @@ +{ + "base": "vs-dark", + "inherit": true, + "rules": [{ + "token": "", + "background": "#11111f", + "foreground": "#DAD6DA" + }, { + "foreground": "#f0fff850", + "fontStyle": "italic", + "token": "comment" + }, + { + "token": "delimiter.parenthesis delimiter.parenthesis.js delimiter.parenthesis.ts", + "foreground": "#f07178" + }, + { + "token": "delimiter", + "foreground": "#c38bf7" + }, + { + "token": "number", + "foreground": "#FF67B1" + }, + { + "foreground": "#ffabb5", + "token": "constant" + }, + { + "foreground": "#B2CCD6", + "token": "entity" + }, + { + "foreground": "#B289F1", + "token": "keyword" + }, + { + "foreground": "#B289F1", + "token": "storage" + }, + { + "foreground": "#ffabb5", + "token": "string" + }, + { + "foreground": "ffabb5", + "token": "meta.verbatim" + }, + { + "foreground": "#ffdf89", + "token": "support" + }, + { + "foreground": "#FF5370", + "fontStyle": "italic", + "token": "invalid.deprecated" + }, + { + "foreground": "#FF5370", + "background": "#562d56c0", + "token": "invalid.illegal" + }, + { + "foreground": "#ffabb5", + "fontStyle": "italic", + "token": "entity.other.inherited-class" + }, + { + "foreground": "#f6f3fc", + "token": "string constant.other.placeholder" + }, + { + "foreground": "#2DE4A1", + "token": "meta.function-call.py" + }, + { + "foreground": "#f07178", + "token": "meta.tag" + }, + { + "foreground": "#f07178", + "token": "meta.tag entity" + }, + { + "foreground": "ffdf89", + "token": "entity.name.section" + }, + { + "foreground": "#B289F1", + "token": "keyword.type.variant" + }, + { + "foreground": "#c38bf7", + "token": "keyword.operator" + }], + "colors": { + "editor.foreground": "#DAD6DA", + "editor.background": "#11111f", + "editor.selectionBackground": "#6A6793", + "editor.lineHighlightBackground": "#161427", + "editorCursor.foreground": "#DAD6DA", + "editorWhitespace.foreground": "#f0fff850", + + "errorForeground": "#FF5370", + "focusBorder": "#291830c0", + "contrastBorder": "#FFCFD4", + "contrastActiveBorder": "#FF70B1", + "dropdown.background": "#161427", + "menu.background": "#161427", + "menu.selectionBackground": "#44dbb5", + "menu.border": "#282646", + "menu.separatorBackground": "#282646", + "editorWidget.background": "#161427", + "editorWidget.border": "#282646", + "peekViewTitle.background": "#282646", + "peekViewResult.background": "#282646", + "peekViewEditor.background": "#282646", + + "textLink.foreground": "#f07178", + "editorLink.activeForeground": "#f07178" + } +} \ No newline at end of file diff --git a/src/pug/index.pug b/src/pug/index.pug index 88c176adf..5df304021 100644 --- a/src/pug/index.pug +++ b/src/pug/index.pug @@ -58,11 +58,13 @@ html const horizon = require('./data/node_requires/monaco-themes/horizon.json'); const ambiance = require('./data/node_requires/monaco-themes/ambiance.json'); const spring = require('./data/node_requires/monaco-themes/spring.json'); + const lucasdracula = require('./data/node_requires/monaco-themes/lucasdracula.json'); window.monaco = global.monaco; monaco.editor.defineTheme('tomorrow', tomorrow); monaco.editor.defineTheme('horizon', horizon); monaco.editor.defineTheme('ambiance', ambiance); monaco.editor.defineTheme('spring', spring); + monaco.editor.defineTheme('lucasdracula', lucasdracula); window.signals = window.signals || riot.observable({}); window.signals.trigger('monacoBooted'); }); diff --git a/src/riotTags/license-panel.tag b/src/riotTags/license-panel.tag index 5d011f061..e5d4d791e 100644 --- a/src/riotTags/license-panel.tag +++ b/src/riotTags/license-panel.tag @@ -52,6 +52,37 @@ license-panel.modal.pad 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. + h2 Arkham theme for VSCode + p + | The ct.js' theme called Lucas Dracula is a rough port of a beautiful theme by Lucas Moreira. + | You can get this theme for VSCode, too! See + | + a(href="https://github.com/lucasmsa/arkham-theme") github.com/lucasmsa/arkham-theme + | . + pre + code. + MIT License + + Copyright (c) 2020 Lucas Moreira + + 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: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + 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. + h2 color.js pre code. diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index fc33da79a..8f174e3de 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -328,6 +328,8 @@ main-menu.flexcol click: () => { this.switchTheme('SpringStream'); } + }, { + type: 'separator' }, { label: window.languageJSON.menu.themeNight || 'Night', icon: () => localStorage.UItheme === 'Night' && 'check', @@ -340,6 +342,12 @@ main-menu.flexcol click: () => { this.switchTheme('Horizon'); } + }, { + label: window.languageJSON.menu.themeLucasDracula || 'LucasDracula', + icon: () => localStorage.UItheme === 'LucasDracula' && 'check', + click: () => { + this.switchTheme('LucasDracula'); + } }] } }, { diff --git a/src/styl/themeLucasDracula.styl b/src/styl/themeLucasDracula.styl index f1df2a79a..fdf7d9914 100644 --- a/src/styl/themeLucasDracula.styl +++ b/src/styl/themeLucasDracula.styl @@ -1,5 +1,11 @@ @charset "utf-8" +foreground = #DAD6DA +text = #DAD6DA +background = #161427 +backgroundDeeper = #11111f +borderBright = #282646 +borderPale = mix(background, borderBright, 50%) /* Frequently used properties */ trans = @@ -19,15 +25,13 @@ br = 0.2rem iconsize = 1.5rem /* Colors used by this theme */ -foreground = #f6f3fc -background = #161526 act = #FFCFD4 acttext = act accent1 = #FF70B1 -accent2 = #FFCFD4 +accent2 = #c38bf7 error = #FF5370 red = error -success = #2DE4A1 +success = #26E19F green = success warning = #f07178 orange = warning @@ -42,11 +46,6 @@ themeDark = true .warning color warning -text = #DAD6DA -backgroundDeeper = #282646 - -borderBright = #4D4985 -borderPale = mix(backgroundDeeper, borderBright, 50%) @require 'hvost.styl' @@ -61,4 +60,15 @@ borderPale = mix(backgroundDeeper, borderBright, 50%) @require 'buildingBlocks.styl' @require 'tabs.styl' +button, +input[type="button"], +input[type="submit"], +input[type="reset"], +.button, +.selectbox + &:hover, &.active, &:active, &.selected + color backgroundDeeper + svg + color backgroundDeeper + @require 'tags/**/*.styl' \ No newline at end of file From 3a56887370a5bc7dc68937e3aefff40413c851a1 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 8 Jul 2020 21:10:45 +1200 Subject: [PATCH 34/86] :zap: New icons for the top panel --- src/icons/texture.svg | 3 +++ src/icons/type.svg | 10 ++++++++++ src/icons/ui.svg | 10 ++++++++++ src/riotTags/main-menu.tag | 8 ++++---- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/icons/texture.svg create mode 100644 src/icons/type.svg create mode 100644 src/icons/ui.svg diff --git a/src/icons/texture.svg b/src/icons/texture.svg new file mode 100644 index 000000000..d014b17bc --- /dev/null +++ b/src/icons/texture.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/type.svg b/src/icons/type.svg new file mode 100644 index 000000000..5ea82fc92 --- /dev/null +++ b/src/icons/type.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/icons/ui.svg b/src/icons/ui.svg new file mode 100644 index 000000000..99d1f8f60 --- /dev/null +++ b/src/icons/ui.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 8f174e3de..674d810f6 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -26,11 +26,11 @@ main-menu.flexcol span {voc.modules} li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}" data-hotkey="Control+3" title="Control+3") svg.feather - use(xlink:href="data/icons.svg#coin") + use(xlink:href="data/icons.svg#texture") span {voc.texture} li(onclick="{changeTab('ui')}" class="{active: tab === 'ui'}" data-hotkey="Control+4" title="Control+4") svg.feather - use(xlink:href="data/icons.svg#droplet") + use(xlink:href="data/icons.svg#ui") span {voc.ui} li(onclick="{changeTab('fx')}" class="{active: tab === 'fx'}" data-hotkey="Control+5" title="Control+5") svg.feather @@ -42,7 +42,7 @@ main-menu.flexcol span {voc.sounds} li(onclick="{changeTab('types')}" class="{active: tab === 'types'}" data-hotkey="Control+7" title="Control+7") svg.feather - use(xlink:href="data/icons.svg#user") + use(xlink:href="data/icons.svg#type") span {voc.types} li(onclick="{changeTab('rooms')}" class="{active: tab === 'rooms'}" data-hotkey="Control+8" title="Control+8") svg.feather @@ -567,4 +567,4 @@ main-menu.flexcol }) .catch(e => { alertify.alert('Could not get i18n files: ' + e); - }); \ No newline at end of file + }); From fdfda86ff4224833fbb62a7eadd0eb0dd719b72b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 8 Jul 2020 21:11:09 +1200 Subject: [PATCH 35/86] :zap: Better display of big toggles at Lucas Dracula theme --- src/styl/themeLucasDracula.styl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/styl/themeLucasDracula.styl b/src/styl/themeLucasDracula.styl index fdf7d9914..d64b2ed90 100644 --- a/src/styl/themeLucasDracula.styl +++ b/src/styl/themeLucasDracula.styl @@ -60,6 +60,11 @@ themeDark = true @require 'buildingBlocks.styl' @require 'tabs.styl' +#moduleinfo .bigpower:not(.off) + border-color success + &:after + background success + button, input[type="button"], input[type="submit"], @@ -71,4 +76,4 @@ input[type="reset"], svg color backgroundDeeper -@require 'tags/**/*.styl' \ No newline at end of file +@require 'tags/**/*.styl' From cb23c5a380bd15d23655c360a6f68d567977a898 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 8 Jul 2020 21:34:27 +1200 Subject: [PATCH 36/86] :sparkles: Quickly create a new type by right-clicking an asset in the textures panel --- app/data/i18n/English.json | 3 ++- .../resources/types/defaultType.js | 21 ++++++++++++++++ src/node_requires/resources/types/index.js | 16 +++++++++++++ src/riotTags/shared/asset-viewer.tag | 7 +++++- src/riotTags/textures-panel.tag | 17 ++++++++++++- src/riotTags/types-panel.tag | 24 +++++-------------- 6 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 src/node_requires/resources/types/defaultType.js create mode 100644 src/node_requires/resources/types/index.js diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 4c4e90126..e5cfbd254 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -254,7 +254,8 @@ "texture": { "create": "Create", "import": "Import", - "skeletons": "Skeletal Animation" + "skeletons": "Skeletal Animation", + "createType": "Create a type from it" }, "textureview": { "bgcolor": "Change bg color", diff --git a/src/node_requires/resources/types/defaultType.js b/src/node_requires/resources/types/defaultType.js new file mode 100644 index 000000000..7a8be4b15 --- /dev/null +++ b/src/node_requires/resources/types/defaultType.js @@ -0,0 +1,21 @@ +const generateGUID = require('./../../generateGUID'); + +const defaultTypeTemplate = { + name: 'New Type', + depth: 0, + oncreate: '', + onstep: 'this.move();', + ondraw: '', + ondestroy: '', + texture: -1, + extends: {} +}; + +module.exports = { + get() { + return ({ + ...defaultTypeTemplate, + uid: generateGUID() + }); + } +}; diff --git a/src/node_requires/resources/types/index.js b/src/node_requires/resources/types/index.js new file mode 100644 index 000000000..e53ebb289 --- /dev/null +++ b/src/node_requires/resources/types/index.js @@ -0,0 +1,16 @@ +const getDefaultType = require('./defaultType').get; + +const createNewType = function (name) { + const type = getDefaultType(); + if (name) { + type.name = String(name); + } + window.currentProject.types.push(type); + window.signals.trigger('typesChanged'); + return type; +}; + +module.exports = { + getDefaultType, + createNewType +}; diff --git a/src/riotTags/shared/asset-viewer.tag b/src/riotTags/shared/asset-viewer.tag index f5954d073..262ab2f00 100644 --- a/src/riotTags/shared/asset-viewer.tag +++ b/src/riotTags/shared/asset-viewer.tag @@ -90,7 +90,12 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") this.updateList = () => { this.collection = [...(this.opts.collection || [])]; if (this.sort === 'name') { - this.collection.sort((a, b) => a.name.localeCompare(b.name)); + if (this.opts.names) { + const accessor = this.opts.names; + this.collection.sort((a, b) => accessor(a).localeCompare(accessor(b))); + } else { + this.collection.sort((a, b) => a.name.localeCompare(b.name)); + } } else { this.collection.sort((a, b) => b.lastmod - a.lastmod); } diff --git a/src/riotTags/textures-panel.tag b/src/riotTags/textures-panel.tag index 1d01b6735..a7259b172 100644 --- a/src/riotTags/textures-panel.tag +++ b/src/riotTags/textures-panel.tag @@ -247,6 +247,21 @@ textures-panel.panel.view this.textureMenu = { opened: false, items: [{ + icon: 'loader', + label: this.voc.createType, + click: () => { + const typesAPI = require('./data/node_requires/resources/types/'); + const type = typesAPI.createNewType(this.currentTexture.name); + type.texture = this.currentTexture.uid; + const mainMenu = document.getElementsByTagName('main-menu')[0]._tag; + mainMenu.changeTab('types')(); + mainMenu.update(); + const typesPanel = document.getElementsByTagName('types-panel')[0]._tag; + typesPanel.refs.types.updateList(); + typesPanel.openType(type)(); + typesPanel.update(); + } + }, { label: window.languageJSON.common.open, click: () => { if (this.currentTextureType !== 'skeleton') { @@ -360,4 +375,4 @@ textures-panel.panel.view document.removeEventListener('dragover', this.onDragOver); document.removeEventListener('dragleave', this.onDragLeave); document.removeEventListener('drop', this.onDrop); - }); \ No newline at end of file + }); diff --git a/src/riotTags/types-panel.tag b/src/riotTags/types-panel.tag index c1bc156a1..1702a89ac 100644 --- a/src/riotTags/types-panel.tag +++ b/src/riotTags/types-panel.tag @@ -19,7 +19,6 @@ types-panel.panel.view this.mixin(window.riotVoc); this.mixin(window.riotNiceTime); const glob = require('./data/node_requires/glob'); - const generateGUID = require('./data/node_requires/generateGUID'); this.glob = glob; this.editingType = false; this.sort = 'name'; @@ -56,23 +55,11 @@ types-panel.panel.view if (this.editingType) { return false; } - var id = generateGUID(), - slice = id.split('-').pop(); - var obj = { - name: 'Type_' + slice, - depth: 0, - oncreate: '', - onstep: 'this.move();', - ondraw: '', - ondestroy: '', - uid: id, - texture: -1, - extends: {} - }; - global.currentProject.types.push(obj); + + const typesAPI = require('./data/node_requires/resources/types/'); + const type = typesAPI.createNewType(); this.refs.types.updateList(); - this.openType(obj)(e); - window.signals.trigger('typesChanged'); + this.openType(type)(e); if (!e) { this.update(); @@ -104,7 +91,8 @@ types-panel.panel.view .prompt(window.languageJSON.common.newname) .then(e => { if (e.inputValue !== '' && e.buttonClicked !== 'cancel') { - var tp = JSON.parse(JSON.stringify(this.currentType)); + const generateGUID = require('./data/node_requires/generateGUID'); + const tp = JSON.parse(JSON.stringify(this.currentType)); tp.name = e.inputValue; tp.uid = generateGUID(); global.currentProject.types.push(tp); From f7f9f011da578912286a162a706b2c4091d8a1b3 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 8 Jul 2020 21:38:58 +1200 Subject: [PATCH 37/86] :bug: Fix lint warnings --- src/riotTags/textures-panel.tag | 2 ++ src/styl/themeHorizon.styl | 2 +- src/styl/themeNight.styl | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/riotTags/textures-panel.tag b/src/riotTags/textures-panel.tag index a7259b172..9cad9837a 100644 --- a/src/riotTags/textures-panel.tag +++ b/src/riotTags/textures-panel.tag @@ -253,9 +253,11 @@ textures-panel.panel.view const typesAPI = require('./data/node_requires/resources/types/'); const type = typesAPI.createNewType(this.currentTexture.name); type.texture = this.currentTexture.uid; + // eslint-disable-next-line no-underscore-dangle const mainMenu = document.getElementsByTagName('main-menu')[0]._tag; mainMenu.changeTab('types')(); mainMenu.update(); + // eslint-disable-next-line no-underscore-dangle const typesPanel = document.getElementsByTagName('types-panel')[0]._tag; typesPanel.refs.types.updateList(); typesPanel.openType(type)(); diff --git a/src/styl/themeHorizon.styl b/src/styl/themeHorizon.styl index aea32ab35..628164a90 100644 --- a/src/styl/themeHorizon.styl +++ b/src/styl/themeHorizon.styl @@ -91,4 +91,4 @@ gradCore = #733041 gradTransition = #2e2233 gradOuter = #1c1e26 #bg - background radial-gradient(ellipse at bottom, gradCore 0%, gradTransition 50%, gradOuter 100%) \ No newline at end of file + background radial-gradient(ellipse at bottom, gradCore 0, gradTransition 50%, gradOuter 100%) \ No newline at end of file diff --git a/src/styl/themeNight.styl b/src/styl/themeNight.styl index 17c5a0b51..1fd8260c8 100644 --- a/src/styl/themeNight.styl +++ b/src/styl/themeNight.styl @@ -70,4 +70,4 @@ grad4 = #1a3541 grad5 = #0C0D18 #bg - background radial-gradient(ellipse at bottom, grad1 0%, grad2 9%, grad3 22%, grad4 43%, grad5 74%) \ No newline at end of file + background radial-gradient(ellipse at bottom, grad1 0, grad2 9%, grad3 22%, grad4 43%, grad5 74%) \ No newline at end of file From 008f9ac6715ba308d62ab87c7db42e17dccd7b09 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 9 Jul 2020 20:58:00 +1200 Subject: [PATCH 38/86] Merge branch 'better-tokenizer' --- src/node_requires/monaco-themes/ambiance.json | 20 +- src/node_requires/monaco-themes/horizon.json | 12 +- .../monaco-themes/lucasdracula.json | 26 +- src/node_requires/monaco-themes/spring.json | 103 ++++--- src/node_requires/monaco-themes/tomorrow.json | 11 +- src/node_requires/typescriptTokenizer.js | 282 ++++++++++++++++++ src/pug/index.pug | 17 +- src/styl/themeSpringStream.styl | 33 ++ 8 files changed, 441 insertions(+), 63 deletions(-) create mode 100644 src/node_requires/typescriptTokenizer.js diff --git a/src/node_requires/monaco-themes/ambiance.json b/src/node_requires/monaco-themes/ambiance.json index ade1e319f..00a48fd31 100644 --- a/src/node_requires/monaco-themes/ambiance.json +++ b/src/node_requires/monaco-themes/ambiance.json @@ -35,7 +35,7 @@ "token": "entity" }, { - "foreground": "#aac6e3", + "foreground": "#cda869", "token": "keyword" }, { @@ -94,23 +94,23 @@ "token": "keyword.type.variant" }, { - "foreground": "#fa8d6a", + "foreground": "#cda869", "token": "source.ocaml keyword.operator.symbol" }, { - "foreground": "#fa8d6a", + "foreground": "#cda869", "token": "source.ocaml keyword.operator.symbol.infix" }, { - "foreground": "#fa8d6a", + "foreground": "#cda869", "token": "source.ocaml keyword.operator.symbol.prefix" }, { - "fontStyle": "#fa8d6a", + "fontStyle": "#cda869", "token": "source.ocaml keyword.operator.symbol.infix.floating-point" }, { - "fontStyle": "#fa8d6a", + "fontStyle": "#cda869", "token": "source.ocaml keyword.operator.symbol.prefix.floating-point" }, { @@ -136,6 +136,14 @@ { "foreground": "#cda869", "token": "keyword.operator" + }, + { + "foreground": "#c296c6", + "token": "pointsOfInterest" + }, + { + "foreground": "#24a2ad", + "token": "type" }], "colors": { "editor.foreground": "#E6E1DC", diff --git a/src/node_requires/monaco-themes/horizon.json b/src/node_requires/monaco-themes/horizon.json index aa9d7579a..609658b6a 100644 --- a/src/node_requires/monaco-themes/horizon.json +++ b/src/node_requires/monaco-themes/horizon.json @@ -8,7 +8,7 @@ "foreground": "#cf4f6d" }, { - "foreground": "#BBBBBB4D", + "foreground": "#4c4d53", "fontStyle": "italic", "token": "comment" }, @@ -20,6 +20,10 @@ "foreground": "#FAC29AE6", "token": "entity" }, + { + "foreground": "#e4b28f", + "token": "pointsOfInterest" + }, { "name": "Function names", "token": "entity.name.function", @@ -36,7 +40,7 @@ "fontStyle": "italic" }, { - "foreground": "#FAB795", + "foreground": "#a96ec9", "token": "keyword" }, { @@ -93,6 +97,10 @@ "foreground": "#B877DBE6", "token": "keyword.type.variant" }, + { + "foreground": "#24a2ad", + "token": "type" + }, { "foreground": "#BBBBBB", "token": "source.ocaml keyword.operator.symbol" diff --git a/src/node_requires/monaco-themes/lucasdracula.json b/src/node_requires/monaco-themes/lucasdracula.json index 4eab75df3..d6f819e73 100644 --- a/src/node_requires/monaco-themes/lucasdracula.json +++ b/src/node_requires/monaco-themes/lucasdracula.json @@ -6,13 +6,21 @@ "background": "#11111f", "foreground": "#DAD6DA" }, { - "foreground": "#f0fff850", + "foreground": "#595d68", "fontStyle": "italic", "token": "comment" }, { - "token": "delimiter.parenthesis delimiter.parenthesis.js delimiter.parenthesis.ts", - "foreground": "#f07178" + "token": "delimiter.parenthesis", + "foreground": "#DAD6DA" + }, + { + "token": "delimiter.bracket", + "foreground": "#C38BF7" + }, + { + "token": "delimiter.square", + "foreground": "#F07178" }, { "token": "delimiter", @@ -23,7 +31,11 @@ "foreground": "#FF67B1" }, { - "foreground": "#ffabb5", + "foreground": "#2DE4A1", + "token": "pointsOfInterest" + }, + { + "foreground": "#FF67B1", "token": "constant" }, { @@ -31,13 +43,17 @@ "token": "entity" }, { - "foreground": "#B289F1", + "foreground": "#C38BF7", "token": "keyword" }, { "foreground": "#B289F1", "token": "storage" }, + { + "foreground": "#FFDF89", + "token": "type.identifier" + }, { "foreground": "#ffabb5", "token": "string" diff --git a/src/node_requires/monaco-themes/spring.json b/src/node_requires/monaco-themes/spring.json index 2dc3f234e..14d48935e 100644 --- a/src/node_requires/monaco-themes/spring.json +++ b/src/node_requires/monaco-themes/spring.json @@ -2,176 +2,173 @@ "base": "vs", "inherit": true, "rules": [{ - "foreground": "8e908c", + "foreground": "#8e908c", "token": "comment" }, { - "foreground": "666969", + "foreground": "#008bad", "token": "keyword.operator.class" }, { - "foreground": "666969", + "foreground": "#666969", "token": "constant.other" }, { - "foreground": "666969", + "foreground": "#666969", "token": "source.php.embedded.line" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "variable" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "support.other.variable" }, { - "foreground": "c82829", + "foreground": "#ce5a24", "token": "string.other.link" }, { - "foreground": "c82829", + "foreground": "#ce5a24", "token": "string.regexp" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "entity.name.tag" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "entity.other.attribute-name" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "meta.tag" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "declaration.tag" }, { - "foreground": "c82829", + "foreground": "#c82829", "token": "markup.deleted.git_gutter" }, { "token": "number", - "foreground": "008bad" + "foreground": "#008bad" }, { - "foreground": "009170", + "foreground": "#009170", "token": "constant.numeric" }, { - "foreground": "009170", + "foreground": "#009170", "token": "constant.language" }, { - "foreground": "009170", + "foreground": "#009170", "token": "support.constant" }, { - "foreground": "009170", + "foreground": "#009170", "token": "constant.character" }, { - "foreground": "009170", + "foreground": "#009170", "token": "variable.parameter" }, { - "foreground": "009170", + "foreground": "#009170", "token": "punctuation.section.embedded" }, { - "foreground": "009170", - "token": "keyword.other.unit" - }, - { - "foreground": "c99e00", + "foreground": "#c99e00", "token": "entity.name.class" }, { - "foreground": "c99e00", + "foreground": "#c99e00", "token": "entity.name.type.class" }, { - "foreground": "c99e00", + "foreground": "#c99e00", "token": "support.type" }, { - "foreground": "c99e00", + "foreground": "#c99e00", "token": "support.class" }, { - "foreground": "ce5a24", + "foreground": "#ce5a24", "token": "string" }, { - "foreground": "ce5a24", + "foreground": "#ce5a24", "token": "constant.other.symbol" }, { - "foreground": "ce5a24", + "foreground": "#ce5a24", "token": "entity.other.inherited-class" }, { - "foreground": "ce5a24", + "foreground": "#ce5a24", "token": "markup.heading" }, { - "foreground": "ce5a24", + "foreground": "#ce5a24", "token": "markup.inserted.git_gutter" }, { - "foreground": "3e999f", + "foreground": "#008bad", "token": "keyword.operator" }, { - "foreground": "3e999f", + "foreground": "#3e999f", "token": "constant.other.color" }, { - "foreground": "4271ae", + "foreground": "#4271ae", "token": "entity.name.function" }, { - "foreground": "4271ae", + "foreground": "#4271ae", "token": "meta.function-call" }, { - "foreground": "4271ae", + "foreground": "#4271ae", "token": "support.function" }, { - "foreground": "4271ae", + "foreground": "#008bad", "token": "keyword.other.special-method" }, { - "foreground": "4271ae", + "foreground": "#4271ae", "token": "meta.block-level" }, { - "foreground": "4271ae", + "foreground": "#4271ae", "token": "markup.changed.git_gutter" }, { - "foreground": "8959a8", - "token": "keyword" + "foreground": "#00c09e", + "token": "keyword", + "fontStyle": "bold" }, { - "foreground": "8959a8", + "foreground": "#8959a8", "token": "storage" }, { - "foreground": "8959a8", + "foreground": "#8959a8", "token": "storage.type" }, { - "foreground": "c82829", + "foreground": "#c82829", "background": "c82829", "token": "invalid" }, { - "foreground": "c82829", + "foreground": "#c82829", "background": "8959a8", "token": "invalid.deprecated" }, @@ -192,9 +189,19 @@ "token": "meta.diff.header.from-file" }, { - "foreground": "3e999f", + "foreground": "#3e999f", "fontStyle": "italic", "token": "meta.diff.range" + }, + { + "foreground": "#dd3b98", + "fontStyle": "bold", + "token": "pointsOfInterest" + }, + { + "foreground": "#008bad", + "fontStyle": "bold", + "token": "type" } ], "colors": { diff --git a/src/node_requires/monaco-themes/tomorrow.json b/src/node_requires/monaco-themes/tomorrow.json index 194a9152d..dd78d9ece 100644 --- a/src/node_requires/monaco-themes/tomorrow.json +++ b/src/node_requires/monaco-themes/tomorrow.json @@ -195,7 +195,16 @@ "foreground": "3e999f", "fontStyle": "italic", "token": "meta.diff.range" - } + }, + { + "foreground": "#C82829", + "fontStyle": "italic", + "token": "pointsOfInterest" + }, + { + "foreground": "#24a2ad", + "token": "type" + } ], "colors": { "editor.foreground": "#4D4D4C", diff --git a/src/node_requires/typescriptTokenizer.js b/src/node_requires/typescriptTokenizer.js new file mode 100644 index 000000000..82193e922 --- /dev/null +++ b/src/node_requires/typescriptTokenizer.js @@ -0,0 +1,282 @@ +/* eslint-disable camelcase */ +/* eslint-disable max-len */ +/* eslint-disable array-element-newline */ +/* eslint-disable no-useless-escape */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable spaced-comment */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// Allow for running under nodejs/requirejs in tests +const _monaco = global.monaco; + +module.exports.conf = { + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g, + + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + + onEnterRules: [ + { + // e.g. /** | */ + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + afterText: /^\s*\*\/$/, + action: { + indentAction: _monaco.languages.IndentAction.IndentOutdent, appendText: ' * ' + } + }, + { + // e.g. /** ...| + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + action: { + indentAction: _monaco.languages.IndentAction.None, appendText: ' * ' + } + }, + { + // e.g. * ...| + beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, + action: { + indentAction: _monaco.languages.IndentAction.None, appendText: '* ' + } + }, + { + // e.g. */| + beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, + action: { + indentAction: _monaco.languages.IndentAction.None, removeText: 1 + } + } + ], + + autoClosingPairs: [ + { + open: '{', close: '}' + }, + { + open: '[', close: ']' + }, + { + open: '(', close: ')' + }, + { + open: '"', close: '"', notIn: ['string'] + }, + { + open: '\'', close: '\'', notIn: ['string', 'comment'] + }, + { + open: '`', close: '`', notIn: ['string', 'comment'] + }, + { + open: '/**', close: ' */', notIn: ['string'] + } + ], + + folding: { + markers: { + start: new RegExp('^\\s*//\\s*#?region\\b'), + end: new RegExp('^\\s*//\\s*#?endregion\\b') + } + } +}; + +module.exports.language = { + // Set defaultToken to invalid to see what you do not tokenize yet + defaultToken: 'invalid', + tokenPostfix: '.ts', + + keywords: [ + 'abstract', 'as', 'break', 'case', 'catch', 'class', 'continue', 'const', + 'constructor', 'debugger', 'declare', 'default', 'delete', 'do', 'else', + 'enum', 'export', 'extends', 'finally', 'for', 'from', 'function', + 'get', 'if', 'implements', 'import', 'in', 'infer', 'instanceof', 'interface', + 'is', 'keyof', 'let', 'module', 'namespace', 'never', 'new', 'package', + 'private', 'protected', 'public', 'readonly', 'require', 'global', 'return', + 'set', 'static', 'super', 'switch', 'symbol', 'throw', 'try', + 'type', 'typeof', 'unique', 'var', 'void', 'while', 'with', 'yield', 'async', + 'await', 'of' + ], + + constants: [ + 'false', 'true', 'undefined', 'NaN', 'null', 'Infinity' + ], + + pointsOfInterest: ['ct', 'this', 'room'], + + typeKeywords: [ + 'any', 'boolean', 'number', 'object', 'string', 'undefined' + ], + + operators: [ + '<=', '>=', '==', '!=', '===', '!==', '=>', '+', '-', '**', + '*', '/', '%', '++', '--', '<<', '>', '>>>', '&', + '|', '^', '!', '~', '&&', '||', '??', '?', ':', '=', '+=', '-=', + '*=', '**=', '/=', '%=', '<<=', '>>=', '>>>=', '&=', '|=', + '^=', '@' + ], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [/!(?=([^=]|$))/, 'delimiter'], + [/@symbols/, { + cases: { + '@operators': 'delimiter', + '@default': '' + } + }], + + // numbers + [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'], + [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'], + [/0[xX](@hexdigits)n?/, 'number.hex'], + [/0[oO]?(@octaldigits)n?/, 'number.octal'], + [/0[bB](@binarydigits)n?/, 'number.binary'], + [/(@digits)n?/, 'number'], + + // delimiter: after number because of .\d floats + [/[;,.]/, 'delimiter'], + + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/"/, 'string', '@string_double'], + [/'/, 'string', '@string_single'], + [/`/, 'string', '@string_backtick'] + ], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'] + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'] + ], + + jsdoc: [ + [/[^\/*]+/, 'comment.doc'], + [/\*\//, 'comment.doc', '@pop'], + [/[\/*]/, 'comment.doc'] + ], + + // We match regular expression quite precisely + regexp: [ + [/(\{)(\d+(?:,\d*)?)(\})/, ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']], + [/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, ['regexp.escape.control', { + token: 'regexp.escape.control', next: '@regexrange' + }]], + [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']], + [/[()]/, 'regexp.escape.control'], + [/@regexpctl/, 'regexp.escape.control'], + [/[^\\\/]/, 'regexp'], + [/@regexpesc/, 'regexp.escape'], + [/\\\./, 'regexp.invalid'], + [/(\/)([gimsuy]*)/, [{ + token: 'regexp', bracket: '@close', next: '@pop' + }, 'keyword.other']] + ], + + regexrange: [ + [/-/, 'regexp.escape.control'], + [/\^/, 'regexp.invalid'], + [/@regexpesc/, 'regexp.escape'], + [/[^\]]/, 'regexp'], + [/\]/, { + token: 'regexp.escape.control', next: '@pop', bracket: '@close' + }] + ], + + string_double: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'] + ], + + string_single: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'/, 'string', '@pop'] + ], + + string_backtick: [ + [/\$\{/, { + token: 'delimiter.bracket', next: '@bracketCounting' + }], + [/[^\\`$]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/`/, 'string', '@pop'] + ], + + bracketCounting: [ + [/\{/, 'delimiter.bracket', '@bracketCounting'], + [/\}/, 'delimiter.bracket', '@pop'], + { + include: 'common' + } + ] + } +}; diff --git a/src/pug/index.pug b/src/pug/index.pug index 5df304021..15db0cf0e 100644 --- a/src/pug/index.pug +++ b/src/pug/index.pug @@ -54,17 +54,32 @@ html // workaround monaco-typescript not understanding the environment self.process.browser = true; eRequire(['vs/editor/editor.main'], function onMonacoLoad() { + window.monaco = global.monaco; + + // Custom themes const tomorrow = require('./data/node_requires/monaco-themes/tomorrow.json'); const horizon = require('./data/node_requires/monaco-themes/horizon.json'); const ambiance = require('./data/node_requires/monaco-themes/ambiance.json'); const spring = require('./data/node_requires/monaco-themes/spring.json'); const lucasdracula = require('./data/node_requires/monaco-themes/lucasdracula.json'); - window.monaco = global.monaco; monaco.editor.defineTheme('tomorrow', tomorrow); monaco.editor.defineTheme('horizon', horizon); monaco.editor.defineTheme('ambiance', ambiance); monaco.editor.defineTheme('spring', spring); monaco.editor.defineTheme('lucasdracula', lucasdracula); + + // Extended typescript tokenizer + const typescript = require('./data/node_requires/typescriptTokenizer.js').language; + // I have no guilt of this solution + // @see https://github.com/microsoft/monaco-editor/issues/884 + monaco.editor.create(document.createElement('textarea'), { + language: 'typescript', + value: '(:' + }); + setTimeout(() => { + monaco.languages.setMonarchTokensProvider('typescript', typescript); + }, 1000); + window.signals = window.signals || riot.observable({}); window.signals.trigger('monacoBooted'); }); diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index e0bb774a4..8bfec73cf 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -14,6 +14,39 @@ shad = shadamb = box-shadow 0 0.25rem 1rem rgba(shadows, 0.15) +@css { + @font-face { + font-family: 'Comfortaa'; + src: url('../data/fonts/Comfortaa-Bold.ttf'); + font-weight: 700; + font-style: normal; + } + @font-face { + font-family: 'Comfortaa'; + src: url('../data/fonts/Comfortaa-SemiBold.ttf'); + font-weight: 600; + font-style: normal; + } + @font-face { + font-family: 'Comfortaa'; + src: url('../data/fonts/Comfortaa-Medium.ttf'); + font-weight: 500; + font-style: normal; + } + @font-face { + font-family: 'Comfortaa'; + src: url('../data/fonts/Comfortaa-Regular.ttf'); + font-weight: 400; + font-style: normal; + } + @font-face { + font-family: 'Comfortaa'; + src: url('../data/fonts/Comfortaa-Light.ttf'); + font-weight: 300; + font-style: normal; + } +} + /* Base fonts for UI */ fonts = font = 'Open Sans', sans-serif, serif fontsHeaders = fontHeader = Comfortaa, 'Open Sans', sans-serif, serif From 1f7aafa89bb18a69281183f960d9006e36d92ab6 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 9 Jul 2020 21:00:39 +1200 Subject: [PATCH 39/86] :bug: Fix linter warnings ar Spring Stream theme --- src/styl/themeSpringStream.styl | 57 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index 8bfec73cf..62eb1bb9a 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -14,38 +14,31 @@ shad = shadamb = box-shadow 0 0.25rem 1rem rgba(shadows, 0.15) -@css { - @font-face { - font-family: 'Comfortaa'; - src: url('../data/fonts/Comfortaa-Bold.ttf'); - font-weight: 700; - font-style: normal; - } - @font-face { - font-family: 'Comfortaa'; - src: url('../data/fonts/Comfortaa-SemiBold.ttf'); - font-weight: 600; - font-style: normal; - } - @font-face { - font-family: 'Comfortaa'; - src: url('../data/fonts/Comfortaa-Medium.ttf'); - font-weight: 500; - font-style: normal; - } - @font-face { - font-family: 'Comfortaa'; - src: url('../data/fonts/Comfortaa-Regular.ttf'); - font-weight: 400; - font-style: normal; - } - @font-face { - font-family: 'Comfortaa'; - src: url('../data/fonts/Comfortaa-Light.ttf'); - font-weight: 300; - font-style: normal; - } -} +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Bold.ttf') + font-weight 700 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-SemiBold.ttf') + font-weight 600 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Medium.ttf') + font-weight 500 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Regular.ttf') + font-weight 400 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Light.ttf') + font-weight 300 + font-style normal /* Base fonts for UI */ fonts = font = 'Open Sans', sans-serif, serif From bfa7a7edc2b7378a118bd584f4b52163157d40fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 23:55:58 +0000 Subject: [PATCH 40/86] :arrow_up: Bump lodash from 4.17.15 to 4.17.19 in /app Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- app/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index dcf476895..1e99c58b4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2639,9 +2639,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.defaults": { "version": "4.2.0", From b2b7773a81dd4a50b1db4de98e955455426014f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 23:57:21 +0000 Subject: [PATCH 41/86] :arrow_up: Bump lodash from 4.17.15 to 4.17.19 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- package-lock.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5a3913fa..ec32c2bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,7 +769,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -4262,7 +4263,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "micromatch": { "version": "3.1.10", @@ -6114,9 +6116,9 @@ "integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==" }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._baseassign": { "version": "3.2.0", @@ -8737,7 +8739,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, From b630b7dd271e5066047e7ee5bfae3a0fc9139a6b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 13:40:53 +1200 Subject: [PATCH 42/86] :sparkles: onbeforecreate injection --- app/data/ct.release/types.js | 7 +++++++ src/node_requires/exporter/index.js | 1 + 2 files changed, 8 insertions(+) diff --git a/app/data/ct.release/types.js b/app/data/ct.release/types.js index ccc2d69d3..e7be106e3 100644 --- a/app/data/ct.release/types.js +++ b/app/data/ct.release/types.js @@ -215,6 +215,7 @@ const Copy = (function Copy() { } else { ct.types.list[type] = [this]; } + this.onBeforeCreateModifier(); ct.types.templates[type].onCreate.apply(this); } return this; @@ -318,6 +319,12 @@ const Copy = (function Copy() { } return parent; } + + // eslint-disable-next-line class-methods-use-this + onBeforeCreateModifier() { + // Filled by ct.IDE and catmods + /*%onbeforecreate%*/ + } } return Copy; })(); diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index 79556759a..c2d5e5a77 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -92,6 +92,7 @@ const exportCtProject = async (project, projdir) => { start: '', switch: '', + onbeforecreate: '', oncreate: '', ondestroy: '', From 585b1ee26c853752467515453fb0329fa83ad881 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 13:41:33 +1200 Subject: [PATCH 43/86] :sparkles: `ct.assert` module for readable checks in ct.js projects --- app/data/ct.libs/assert/AssertYield.png | Bin 0 -> 85048 bytes app/data/ct.libs/assert/README.md | 43 ++++++++++++++++++++++++ app/data/ct.libs/assert/index.js | 39 +++++++++++++++++++++ app/data/ct.libs/assert/module.json | 11 ++++++ app/data/ct.libs/assert/types.d.ts | 15 +++++++++ 5 files changed, 108 insertions(+) create mode 100644 app/data/ct.libs/assert/AssertYield.png create mode 100644 app/data/ct.libs/assert/README.md create mode 100644 app/data/ct.libs/assert/index.js create mode 100644 app/data/ct.libs/assert/module.json create mode 100644 app/data/ct.libs/assert/types.d.ts diff --git a/app/data/ct.libs/assert/AssertYield.png b/app/data/ct.libs/assert/AssertYield.png new file mode 100644 index 0000000000000000000000000000000000000000..6cde8baab59280acdc6fbe333fafda0807855ea7 GIT binary patch literal 85048 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 0) { + console.error(`%c Failed: ${failed}, passed: ${passed}.`, 'font-weight: bold;'); + } else { + // eslint-disable-next-line no-console + console.log(`%c✅ Failed: ${failed}, passed: ${passed}.`, 'color: #3c3; font-weight: bold;'); + } + failed = passed = 0; + }; +} diff --git a/app/data/ct.libs/assert/module.json b/app/data/ct.libs/assert/module.json new file mode 100644 index 000000000..9f088cfc1 --- /dev/null +++ b/app/data/ct.libs/assert/module.json @@ -0,0 +1,11 @@ +{ + "main": { + "name": "ct.assert", + "version": "1.0.0", + "packageName": "assert", + "authors": [{ + "name": "Comigo", + "mail": "admin@nersta.ru" + }] + } +} diff --git a/app/data/ct.libs/assert/types.d.ts b/app/data/ct.libs/assert/types.d.ts new file mode 100644 index 000000000..704bc4126 --- /dev/null +++ b/app/data/ct.libs/assert/types.d.ts @@ -0,0 +1,15 @@ +declare namespace ct { + /** + * A tiny module to simplify tests in ct.js projects. + * @param condition may be either boolean or a function — other values (numbers, strings) will fail. Functions are executed first and then tested against their returned result. + * @param message The message to show in console. + */ + function assert(condition: boolean | Function, message: string): void; + + namespace assert { + /** + * Displays a summary with a number of failed and passed tasks, and resets the counter of failed and passed tasks. + */ + function summary(): void; + } +} \ No newline at end of file From ed0d0b30bbbd6046c51d4923959e39a6900c7e50 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 13:44:24 +1200 Subject: [PATCH 44/86] :sparkles: module that allows you to call parents' code and keep things DRY\nAlso unifies input fields at module settings and asset extensions --- OurRiotDocFormat.md | 2 +- app/data/ct.libs/.eslintrc.json | 7 +- app/data/ct.libs/inherit/InheritanceTest.ict | 369 ++++++++++++++++++ .../i15293c51-6bdc-4a62-941e-0b08656796a7.png | Bin 0 -> 2475 bytes ...1-6bdc-4a62-941e-0b08656796a7.png_prev.png | Bin 0 -> 2201 bytes ...6bdc-4a62-941e-0b08656796a7.png_prev@2.png | Bin 0 -> 4286 bytes .../i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png | Bin 0 -> 394 bytes ...a-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png | Bin 0 -> 948 bytes ...1c6d-4020-8aa5-1268faa5cbeb.png_prev@2.png | Bin 0 -> 1412 bytes .../i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png | Bin 0 -> 2372 bytes ...1-1d13-4f67-ac3d-cccec7b01cc4.png_prev.png | Bin 0 -> 2374 bytes ...1d13-4f67-ac3d-cccec7b01cc4.png_prev@2.png | Bin 0 -> 4818 bytes .../InheritanceTest/img/r428ea16b4401.png | Bin 0 -> 7715 bytes .../InheritanceTest/img/re8d64a493cce.png | Bin 0 -> 7859 bytes .../inherit/InheritanceTest/img/splash.png | Bin 0 -> 7859 bytes app/data/ct.libs/inherit/README.md | 38 ++ app/data/ct.libs/inherit/index.js | 50 +++ .../ct.libs/inherit/injects/onbeforecreate.js | 40 ++ app/data/ct.libs/inherit/module.json | 15 + app/data/ct.libs/inherit/types.d.ts | 75 ++++ app/data/ct.libs/place/module.json | 10 +- app/data/i18n/English.json | 3 +- app/data/i18n/Russian.json | 1 + app/package-lock.json | 6 +- package-lock.json | 15 +- src/node_requires/exporter/rooms.js | 3 +- src/node_requires/exporter/types.js | 7 +- src/node_requires/exporter/utils.js | 51 +++ .../resources/types/defaultType.js | 1 + src/node_requires/resources/types/index.js | 35 +- src/riotTags/modules-panel.tag | 43 +- .../project-settings/project-settings.tag | 2 +- src/riotTags/rooms/room-tile-editor.tag | 2 +- src/riotTags/shared/asset-viewer.tag | 6 +- src/riotTags/shared/extensions-editor.tag | 128 ++++-- src/riotTags/shared/type-input.tag | 66 ++++ src/riotTags/shared/type-selector.tag | 26 ++ src/riotTags/type-editor.tag | 2 +- src/riotTags/types-panel.tag | 4 +- src/styl/tags/shared/extensions-editor.styl | 5 + src/styl/tags/shared/texture-selector.styl | 4 + src/styl/tags/shared/type-input.styl | 5 + src/styl/tags/shared/type-selector.styl | 2 + src/styl/tags/textures/texture-selector.styl | 2 - src/styl/tags/textures/textures-panel.styl | 7 +- 45 files changed, 930 insertions(+), 102 deletions(-) create mode 100644 app/data/ct.libs/inherit/InheritanceTest.ict create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/r428ea16b4401.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/splash.png create mode 100644 app/data/ct.libs/inherit/README.md create mode 100644 app/data/ct.libs/inherit/index.js create mode 100644 app/data/ct.libs/inherit/injects/onbeforecreate.js create mode 100644 app/data/ct.libs/inherit/module.json create mode 100644 app/data/ct.libs/inherit/types.d.ts create mode 100644 src/node_requires/exporter/utils.js create mode 100644 src/riotTags/shared/type-input.tag create mode 100644 src/riotTags/shared/type-selector.tag create mode 100644 src/styl/tags/shared/extensions-editor.styl create mode 100644 src/styl/tags/shared/texture-selector.styl create mode 100644 src/styl/tags/shared/type-input.styl create mode 100644 src/styl/tags/shared/type-selector.styl delete mode 100644 src/styl/tags/textures/texture-selector.styl diff --git a/OurRiotDocFormat.md b/OurRiotDocFormat.md index cd0f766aa..913ac42f7 100644 --- a/OurRiotDocFormat.md +++ b/OurRiotDocFormat.md @@ -25,7 +25,7 @@ At `./src/riotTags`, use this syntax at the top of the file to document tags: A description of a tag's method that can be safely called for inter-module communications. @property propertyName (type, typeSpecificator) - A descriotion of an exposed property of a tag. + A description of an exposed property of a tag. ``` Optional attributes are inside square brackets. diff --git a/app/data/ct.libs/.eslintrc.json b/app/data/ct.libs/.eslintrc.json index d64caeed7..b2d14ecc8 100644 --- a/app/data/ct.libs/.eslintrc.json +++ b/app/data/ct.libs/.eslintrc.json @@ -1,7 +1,12 @@ { "globals": { "ct": false, - "PIXI": true + "PIXI": false, + "Room": false, + "Background": false, + "Tileset": false, + "Copy": false, + "Camera": false }, "rules": { "object-shorthand": 0, diff --git a/app/data/ct.libs/inherit/InheritanceTest.ict b/app/data/ct.libs/inherit/InheritanceTest.ict new file mode 100644 index 000000000..a4f81c666 --- /dev/null +++ b/app/data/ct.libs/inherit/InheritanceTest.ict @@ -0,0 +1,369 @@ +ctjsVersion: 1.3.2 +notes: /* empty */ +libs: + place: + gridX: 1024 + gridY: 1024 + fittoscreen: + mode: scaleFit + mouse: {} + keyboard: {} + keyboard.polyfill: {} + sound.howler: {} + assert: {} + inherit: {} +textures: + - name: robot_greenDrive1 + untill: 0 + grid: + - 1 + - 1 + axis: + - 79 + - 60 + marginx: 0 + marginy: 0 + imgWidth: 158 + imgHeight: 120 + width: 158 + height: 120 + offx: 0 + offy: 0 + origname: i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png + source: >- + /home/comigo/Downloads/kenney)robot-pack/PNG/Side + view/robot_greenDrive1.png + shape: rect + left: 79 + right: 79 + top: 60 + bottom: 60 + uid: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + padding: 1 + lastmod: 1595996924807 + - name: robot_redDrive2 + untill: 0 + grid: + - 1 + - 1 + axis: + - 90 + - 73 + marginx: 0 + marginy: 0 + imgWidth: 180 + imgHeight: 146 + width: 180 + height: 146 + offx: 0 + offy: 0 + origname: i15293c51-6bdc-4a62-941e-0b08656796a7.png + source: /home/comigo/Downloads/kenney)robot-pack/PNG/Side view/robot_redDrive2.png + shape: rect + left: 90 + right: 90 + top: 73 + bottom: 73 + uid: 15293c51-6bdc-4a62-941e-0b08656796a7 + padding: 1 + lastmod: 1595996927142 + - name: magicMid + untill: 0 + grid: + - 1 + - 1 + axis: + - 0 + - 0 + marginx: 0 + marginy: 0 + imgWidth: 70 + imgHeight: 105 + width: 70 + height: 105 + offx: 0 + offy: 0 + origname: i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png + source: /home/comigo/Downloads/kenney_platformerbricks_updated/PNG/magicMid.png + shape: rect + left: 0 + right: 70 + top: 0 + bottom: 105 + uid: 387786ba-1c6d-4020-8aa5-1268faa5cbeb + padding: 1 +skeletons: [] +types: + - name: AbstractMonster + depth: 0 + oncreate: |- + this.speed = 10; + this.direction = 180; + this.depth = this.y; + onstep: |- + this.move(); + + if (ct.place.occupied(this, 'Solid')) { + this.x = this.xprev; + this.y = this.yprev; + this.direction += 180; + } + + if (this.hspeed > 0) { + this.scale.x = 1; + } else { + this.scale.x = -1; + } + ondraw: '' + ondestroy: '' + texture: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + extends: + ctype: Monster + uid: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + lastmod: 1596073035148 + - name: Wall + depth: 0 + oncreate: this.depth = this.y; + onstep: this.move(); + ondraw: '' + ondestroy: '' + texture: 387786ba-1c6d-4020-8aa5-1268faa5cbeb + extends: + ctype: Solid + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + lastmod: 1596073038085 + - name: Monster_Red + depth: 0 + oncreate: this.inherit.onCreate(); + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 15293c51-6bdc-4a62-941e-0b08656796a7 + extends: + ctype: Monster + inheritedType@@type: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + uid: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + lastmod: 1596073030644 + - name: Monster_Green + depth: 0 + oncreate: this.inherit.onCreate(); + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + extends: + ctype: Monster + inheritedType@@type: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + uid: 78739be5-145e-45aa-8e4b-473b92c19551 + lastmod: 1596073032003 + - name: Monster_Red_Squished + depth: 0 + oncreate: |- + this.inherit.onCreate(); + this.scale.y = 0.5; + this.speed = 20; + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 15293c51-6bdc-4a62-941e-0b08656796a7 + extends: + inheritedType@@type: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + ctype: Monster + uid: 48c1951e-071c-47ef-a710-ef2ef0978017 + lastmod: 1596073029453 +sounds: [] +styles: [] +rooms: + - name: Room_428ea16b4401 + oncreate: >- + console.log('Each bot should run from wall to wall, and everything below + this text should be green.'); + + ct.assert( + ct.inherit.isParent('AbstractMonster', 'Monster_Red_Squished'), + 'ct.inherit.isParent works with two types (two strings)' + ); + + ct.assert( + ct.inherit.isChild('Monster_Green', 'AbstractMonster'), + 'ct.inherit.isChild works with two types (two strings)' + ); + + ct.assert( + ct.inherit.list('AbstractMonster').length === 3, + 'ct.inherit.list gets all the monsters' + ); + + ct.assert( + ct.inherit.isChild(ct.types.list['Monster_Green'][0], 'AbstractMonster'), + 'ct.inherit.isChild works against a copy and a type' + ); + + ct.assert( + ct.inherit.isChild(ct.types.list['Monster_Red_Squished'][0], ct.types.list['Monster_Red'][0]), + 'ct.inherit.isChild works against two copies' + ); + + ct.assert( + ct.types.list['Monster_Red_Squished'][0].depth !== 0, + 'Monster_Red_Squished has a proper depth, which means that its grandparent\'s code was executed' + ); + + ct.assert.summary(); + onstep: '' + ondraw: '' + onleave: '' + width: 700 + height: 600 + backgrounds: [] + copies: + - x: 0 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 70 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 140 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 210 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 280 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 420 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 490 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 560 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 70 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 140 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 210 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 280 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 350 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 420 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 70 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 140 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 210 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 280 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 350 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 420 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 70 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 140 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 210 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 280 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 420 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 490 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 560 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 210 + uid: 78739be5-145e-45aa-8e4b-473b92c19551 + - x: 441 + 'y': 380 + uid: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + - x: 210 + 'y': 280 + uid: 48c1951e-071c-47ef-a710-ef2ef0978017 + tx: 1 + ty: 0.5 + tiles: + - depth: -10 + tiles: [] + uid: bf117e14-2f8b-4457-b418-428ea16b4401 + thumbnail: 428ea16b4401 + gridX: 70 + gridY: 70 +actions: [] +emitterTandems: [] +starting: 0 +settings: + authoring: + author: CoMiGo Games + site: 'https://ctjs.rocks/' + title: Inheritance test + version: + - 0 + - 0 + - 0 + versionPostfix: '' + rendering: + usePixiLegacy: true + maxFPS: 60 + pixelatedrender: false + highDensity: true + desktopMode: maximized + export: + windows: true + linux: true + mac: true + branding: + icon: -1 + accent: '#446adb' + invertPreloaderScheme: true + fps: 30 +fonts: [] +scripts: [] diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png new file mode 100644 index 0000000000000000000000000000000000000000..7105328fdf38b941ac51e9f63905c2e51715c134 GIT binary patch literal 2475 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 literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ddf4f8de62cb88701a5884b87e6b6d51644590 GIT binary patch literal 2201 zcmV;K2xj+*P)tsI|{Mukc+sLLNtsv+$7-$NYKl?MP!lZ)A z+A`n|f*?&)1;Hl7gh10kNJW~^gpg1gR&r@dOPe%p5|TPM$CvMR(0ekQxN%}bfpBEgPm~ z!Eu~59#r7vjKS|S;L)_zZOoVijd2d{F`)VU^l#U7-Lt_L&b`7$&VQjn73`0uWgm$r zl5mAs-2p|MtttwF0sop#WQq#SAB3tK*91og8$Ea8Nz1Z+KOax3YudR;i_6ObQ21KS z0PqAW9IhKD*vR)j|4J;8{O+bx`DFn08wR|dRhmJTy|}7D3sk(uhE5;vikW6-kp#Hr zV+^kshVEVSKD2?itl|xJ?OV@uIYMkp02Ecb$ofw`-NnGRB>)g?y8|Ff02I{(D94yf zn6MpzGF^i!fcc~aP2mU{9yw;(F8p2p{Q;DB zbb|ZK0mp%P=Ne|-`VrtdTh|1)5-eICB>BN>06dh;x- z;ejm-KT9wmD1-}VY8;UZ-v%6O3j+W^soD)a^f55igXHxdShok4 zb%NfOc4+kvz>P-{fB#*G7Wz(?Dvu94P=i3gG1o4L*i?9|!;7U2r80ux%Tbz4S$k-V{9^Exkwj z+{GpkH66U(7s23^iKOGg5#%JZCYEc)gD&Lr`QY_>mwoUgc!+161EC59&op}AQyB{0 z5a~Phbk~2T%+Bv$oR zn50a09}EVe>v~%1WCpu;@6JmLG$wE+q#|Gx=4D6%@Z2XK>gu0bcp4KzV0wBQ@pwF! zKtA+10kL!EPN?ca*H=*#R99E0|DS8)mk#L&RU5gc9H*X0NH0a9G0<~h@4|jPmgq#GJM59qTcQF7I87LHbJo$Yg?K8~=o@mtbv6kF(KJWp{ngCtw z;K76G5Jij=BNQph%gYf8g)+u&+_-`9@o~vuYin!9*zoW$MnQmjLsk|i@53cK;VXdvL zpU^bz&s3Pv0xE-NXJ^-?9w3iTK7k~jYJyM}xog+16sYPT8^eHq^bRczI+z%u`QrTp#^N4ZECx{mMe4Y$u z36NBgm1{2{#J_FZ?vQ=TW5ob3gcgGh021N^p!w)jX=*jgF##m+!Tp&P*adLpE%Px0ek~lF=5+}y9HEB{2 zfc7I0R$$(7U6%@xKM5gzIW{)d-PhMA9m(o{gi|0OHp@0805*#xHtHTF4cI8;H_N%( bUIYFILXGOi+rVN`00000NkvXXu0mjf47v~6 literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png new file mode 100644 index 0000000000000000000000000000000000000000..7a2760d4ea62386527a730b3e081b1962df7d353 GIT binary patch literal 4286 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 bTv0fw?|pue*me8gUjoq8e56sWW*`24ZNmt@ literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png b/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png new file mode 100644 index 0000000000000000000000000000000000000000..968f914c76abb8da2f1fea260a6507afcc595c4c GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^Za|#L!3-ofa4ii5QY`6?zK#qG8~eHcB(ehe_5nU2 zu8U@F-gM~j;)Pqb967pT$IdzB>$V?1v1HY@1w9+r?c2Zk@R3Dxx6G?wziiX?Rl9cY zJb7yQ_8q(C9-C9VZujXk%Nn6*Ol(dGR`ypl2KN&}ns=BzN#R@bt=-{tI- z_%y}UGBWX@$}^+%rKNWksvOr;wfy|_u*lM~t{_WSO}?vLB}FVdQ&MBb@0QS7AI{*Lx literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png b/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..2f940a5cac9c4ad7043b431be9c24ac6fb0dba8d GIT binary patch literal 948 zcmV;l155mgP))vaBSAz#JPIO8G#V9e3Puz>=tc0V|G=9U_2SKcV2%o2B6v`Ypf?de zh>1k-E(k`+W@nO}o}O$=&$=;RYbuA$5~;rI<<-{I>sQtF1U7LV-F;>^0q+^~+U<*i znw$)Df5-%|0=NZu22h7fFL68s`4!-2ko>%pfxHLsBE*{@ufpsF*bA^1U@y?i9iN(; zy#RXw_5$P;pA2L#z+Qm80DA%U0_+9Y3$PcU?&H10u@_)3z+Qm7;*){w1=tI)7ho^I zUVyy-dja+Wz1;Dsxx3~C9^ZShEfjF|)_L-RM+Q3f$4B363j#*2pC;Eu208*B+#K5$ z1l+iPg{q8T@# zCsTkrB?J&2Q$PVoKIl!6rw&Va{RJfN5?H-9;g%5~`UJuu1yxqaOa}AQKz~j*EJjE` z9Z`@2GQaw?mO_8huzCX}cz`QD*cHGJeCS9cadGiV61Ic@Cwn!hp!y{$l?oOY7g4X* zk$y{&elhd?-}5R`*F&fVbh(^im)4U#Dh8~u@W0Z<1eg53eil`^DkwN3jt(&IRWUeHLU zHJUR`bd7+~J5OwJD1_Rf@*>(dxca_~F?l7T=b%yHO|4 ztd>D20a=nu?eect=ZbM8=vc!xEc|{S)&439g~En8=jZ3qh#RoYfGSGeW*sIre zqvF@JLlos7?{3ijQjU|g8*o{c{qyEm|5sZHSI(Xx*F^@pWaIDN+fK6YRG3qOi|Hi$ zPI2v_N+oT1JB*wdCf7vBrZW*l^Uhgr(*~qs~gpeAkYtV#dXxNdzOUUeWZ6 zOe)w^Yw>lR-pCis?mC?qrmZ4WQ4U){F9DdMZI>uq6f|zcirGw>{s!cs_4RWMxf$= zBMu)*Y=CCOXdT`f{1Ce{3o~ntUF#s`_#i17yXwNS;eg@1t+y_3uwyu|A+s_TD%FE zX`iY=vOjbm9O9Y}{@zo_Qps@_?IRnz!in+pph(B{r`R19ojYLbWwTvf{d>AHrFec?-{`$r;P;4uvH$pHVl|_QRsu>StP!0x5)= z#!mMU#eaMYg{$W^jYv};F}XX)5@Sec?u(0x`bZcn)i3Q22w^#B1_p{u__BY@y9C=EHV4EMBLL8&G8^yeXLTcCgJk_xX4KR8M$13d#7ek#l*M?BAP2` zG};rC^-8CyD9*e=8RU$oA7_~3ZF={P8nuFZ_U7D)qk{U*^&tyJmgjvayj|WNH@!8d zY=$2mdZyaE+uM3Q{-KM5c`8-KV0q|d7Bj}KOWvbTq0Ut78FdT%v#q3jHTgz-gt{}( z`DlSD{sqj(l2ACwf;-}xlP6ANq)ZIu4d88^=le0=fFV&h*~M%Tcm zyK>!4Yqxrr2PykV^4W{A>Qc0A&8|wTo2&XWC*4B5P)I^qv$b{THLVwF<8$(-8ju5@ z6T6K1jyw6zb}E#EU3s04n|I`DG;{G6H-)x#_EMeW%)mE-9=tjS=EyE!={!i4PnEQK z;nQPcHi!DR{qtvuUDFGlqJ>l4gZHHnwa?-fA63u6lhNWAywc4QRlTf0!;!8_+zsd( zh?zdFQB(G0V>6mDM2~v9lzsV>n!D3M)ICG2`6uqP$!%#ISNlADl!ATSo&EZIn)$S8 z5cARJC=<{Qj|Sl-lq2N#@kadY#v3q`9l$&WTauSf9<>H0S3x8~W{y-x1OXcYrX!FE zzHVx$8m_bh~3VOV2Bou{#@ZF*G{ vEOd{Td=HvQFhHfJN={jLB#5khJ4k7xZ2jwghi literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png b/app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png new file mode 100644 index 0000000000000000000000000000000000000000..b4602398abad8ca855da69f61c2904b27cd3a9e0 GIT binary patch literal 2372 zcmbtW`B&0u7snkfmnO7wK^xzB#|3F3OH0zpL|jT!aThIj8UsbjuW5>wD}`n*8zELY<51ZPV3q6?=d|k2hb2%*NK!iY5G1pvL`FL?QB}quZPKK# z*L`8v0pGszc96poOta!lwmNXD*9fOdS-b%r|2k4XY;epm+M9Vw{pOgNW%om>Fqgpk zZLu?Kd-s^AXE5B@EN-)izx5Frvz2Ghn`<}Z32!!N7eYQBL6(8U2wdxlM>q)3Me48zu#Z0D)P9uCMe%qEIgHfCx~;dmeZ3+WwQGUfEd*22jp;R`Q|vdFHo zUiK!y9Lv?v3UgVu74dTRht0V6doJV=bR{k0-;z#i^EED4`7tB!@_(!oiLsMix*vb{ ze{|Et&tmX|?>3p7Qt`;r}RY4#f9|EVR@iPqBMSIN#)`=cSQmk7|znJOih%?ph=f7Zxst72L9Rs|C){Q=ufr zI;n5;>dnu~Q6o(r1$k$yYS6WCOo2|uooiEZlv_VlI}J}(xrh4uHeJPCUgz4WX+d}V zpR+fd=5dgMTK6lSznvJiA)|HmF~D^cdp%T(6N#<4FhUNouWe<|mlw()TP^*hhHEe- zEo)E%bzzu1cJskg;2A?+$ZPAhO9>GvW8%M)>x2Sj;k$zuLC`r{`8f&b|A!K30Q?Vw zBtVt}$SGo4G3Vj`IpRk-ag3)jft^3*d1D){HvZk%kWd?d+Ec$D5&b0)mwz%c*fMyq z;6*xxl0Ul|;_B*3Yi=HW4D)!LYUKMiGc(9&WjvtOd;A`H;|aU8Lmsf6-`6~OWyQ0* zjac_+AX!CLQo>$BN!9Y@o?*5S?6WkP12eH08ku{4?CK|sn}>&q( zOTl9=-nCe56m;&|fOl@kg{VT0^`BQD;8splbty_eVlXgXD`=3rCMO@!M(O-9kacBE zeS|vzjj{nqf@*_j2~0oFP5+q&C!oC5VHi z`0U(3U4zUko0V>*sl94_v{FvTorP^nfmG>$KIrIN(+xr zvWn5UaBA-B$FD1ZTqeMJL7UW`n<(vjDq2~}x^`Ia4ie=HzD$ZIhY$)HN&+cv@1~?O zvI+|e!>Mjqtk#a_gAMlnCX22oCXPH#)&+R%gq$#p?=PV&yk9PbJXhnhB7SG_W{J?B%UCI{|Skw!M|GW}mI`76n&T2*Yw-^JLv^clq z_lv%8bTb6C9~WB_zL*n#7Y#mb^0oPkC>|m2PKtYf3}<0cBm8Yh)T?`%ch=wz2JR6N z5yeUw8xPAI;<^-8Td*(`3gv8=x)D2xTY8hVkT%PxW=`2i6s?u1Rt_DzccXt};z@|1 zC!F52B3hdPhQ59?4^L7kr4AWa4q27n*+?*OIN2FVZ25_`>deQxQh9>ZY9a>a-ibef zH*c8XbE_Ro;q8U4jXvh3M-~r-Rk}Hvjjf>mNEu}{kHT~6RM|goOZ-KORefF6bnT4t z(1$*FRIOnVAADNd{RNTz?wznaQrEm;%&>`QXK$Ya%S(PZbMIC4jZN#E?vXY%S0*i8 zURu&g^FfZi)L4BEofQ!Ln#E$xx2Ja5mY<}*dSzEf*EAZhcY|ME`oJnKoZr`ktHd^p z?j&QlZaZ42Vd#E8f5MvXtt%5&Dh_4?YKuAG|DjE{` zvmg00BqYRy1S{$fv<|AQqbNmdD<-9&uFKMNS<~LW_+l zCjjPwz#QNZtHBb+nC1;p6d_A8j0OXY1{neqN`5@&JOYj|V{DZJ(*XZhGO7H-yJEgF z`0&GV+E@c%aCFq9y5iqrz)u)so|2zf#v293U^baxvsfVtrGt>I5})Fn{bpWG{&sNB zr^6Zmr*B;N0t5aC#u`;cVX=4&$z&3$s+Q5D(zYTHOeS+lHxsJ}Ncw4zWLPa`NTS3! z=Sy5wzw^C4NB+VFXGTpt5I>`G_1Q=?fv>{bf|CS6d? z!YTHPf1ht-BD)cZMC|1C?I;9*EX$fF%OR9s>a4bqD&jGA`rVPQ$71ob(O9$`dsk!+ zlaa!}R5k!eNt?w2v(fl%cKW?HUka=QeyQ^ReZ1#G&mbFMu~^Cq0ELvJuKs2Av$5BQ z;?dYZMO;^=O^N;#2IUF?T`jxK{yX-gt7nIx@+<(T0w{?jzsbXKkm zZju8EQLI{PY5-K|C=ApXkiQC4i{6Dir>Y^KMB@m1myigApvIG0lH*CG5Is|Cs8J%J zpQOAfO6$|d{M!h!2+;V-Xn^dnL$rR9RI37jnn=QbV+y|OlZY((kqCsLBodnOTq$l= zptr3$0MyIdY_?6uT5KHv!6-v=)WhW214~yM1cRi3sJFu+6VPFEV6VABLvt>=iVp%l zB=gKOXx6;fQjZR+6Otgn9}D5V)kVZra_V{xP-BUKyBFWV!sY)U87}{1hyXF0&2YI~ z1<&>f5Yzrv)PJ@YraD^+fc-WXzSMF6Hrc2F5K6}J*1|1}uP)}Km&;(q3;TMpOEwe1 z5K|Rg^?iiPzL|9Z-hOS!e|>7;gEJ!_`d3m21o{*@*kFy(0rb>2XWTxy zdLM7hy^pYxJ<&d~yAzMO+A`M9MON_o>^Qu!;4u5fD=!UA{^4%}T=^u?g+d{O!(k1C zVvVOK=>X6mlRQU2=t2M(jmE-RAPQ*v+Htfz(gRtP@Tg-So^0w!eOaP#6<55s@wRU! zXCCObH{rR~9>~JlSe@*la5+CWfvB1oW}pA+mxltk=TZ|PGTQ6)mSlJ?uh!SsXACsS zcmko&FrYnKo_YjrC!c^Iim*#Y9BXVxZ^It#apNls81;ULl|=5`WfVjlb++NCb018i zjOm~sZ!S#28w;)FfWwCm4+(;h0w5R+Vs&-3P=;=~j$*!{p&@0k!C=q;&`af(8vtov&CSiQ+wF)%BAB0_&w5Z(QxnLmu~-ZX3k!&oH`e9<9)OmX7P#GRjk1(M zXJ=<|=gys!fnFA*KqZjCEG7h8hY!H>;urIVK*bn9<*UEHKcjBb z7bat4W7@aKM6aHn9<9{rLf5ZdyOv_XMg|Z7w7!@mNI78tsgrpDh=wCr_WQNIWus8# zXmC1V$~myvA_ORTb#--RC6LL*+EzH3eb7UwQ!a2g92)(pw51D$$lbeZC(G>&!0PR}%>kgq*4^Em#ej*42`nuw zX|YTUAS2V|)~#FGVQRYrF!RRf<^WKf_Vx8?4U8@n`xN6;a_IWOg9o(=kn4Ib0EjI` zC&9i0=ybc00G;C6+uJoSQ+XqL-@ku9WiaId3IhU(z`1wto)#X} zEQ7`70P1_ZSFgi&`TEuX(B|YZ9RNBhZfuY!tLp$z4oN?dRO1{_3;?8i==L1u&R;>? zw_F$i#XMl++FVCrVofpU04jqWW$Fb!o|wi*fBh$7zGbM2I?Rq7Ir4J=zt$C}Hh6h? z`9W;XxR)BAbc0K2h8%rea6fYlG$7H1F(LCcS8WE7TkS+Bionb)7T>;FJ2zis&}h%}WL<~{ z6-D_b>+I}&$Y!&>&ls!IZ=y3lb%YP{&LEuse(>!pmu$O#mZx--c8v`_j`nn2lV z>by}$&iPxxVDOX{HNCyPCqz;F6JyL(s=$Xlc&^%jM`@=CdgN-B9j-&~URBF|HvDYUuboZkcR zir?=azI5qQG?h0!9*@y(w?EAo`wHj$QO1~?G4?>Oei1b(>*vY^IVh_kUe`U0Wn8B%!QpuWN*Bk1kS1uFQ<=?J6=7Q3%(}<|4!!nPq2%>}&5G zvafHSKjHhsIp^^>kH$Kb|d7 zLb?7W*!T~-m|5W<`=^Y-YP>KE4dAJs**`h08~_9@wgyjCY90av;Q2t9SZGim>->|h zP@K@qF4aV}cv^&uj@`*j^f%9L>O|gxj6@kWY7be=vRC(1R>$wrymONnKD|->AA>)l zaMt4ZkK9+H?RtNtxf+BnKEB8gb*OMEAz4+9ZZsF-@p z2!f+7k}*>C!dDbFaCXGT9@`P^7apbSQI6xv4StPcP?~6Z-nFB6GiuJ&SuFG`6ZDcbkzUOPzSMLg6Mq`QOE(}+5K>((56@Kr%yF%_e~&kf{#7mw#q zRYw-;W!QD2@pn{QcY03q z%0rLva;#^(#6stc3032d@6JohbAV}mF^dHR!#6y+Xvt_lSqa^Lo_opEtiyZUu{b-24}%P6~_kirqf5K#v`dtOlz3s3ws_DrzK ztIu&oz7rYXKOeMyHY?}D3j;x0qhU8lhg-xtIXYW-hNEh?BGT%pue7Z4lco4O->|(I z+!_wLf&S=;x0G+oJfA8Vpp=qmjV6?tpVKaMkR*mZwI z*sRrWwV;+`n;5#HpVsjAi^V0A|A+?OO(fAWwj)=Fjarx;={uw~amB>M^l*hz$X3`V z)B?#=m4@-e&#sTQ`cCYYws;+GtjEg~-Yu{%kf5=u*_J^E{o6U3Z=Y__n|%!vSQh?! z_w$IA-JJBx>~_K795bOQ1v$AVKWmsJT+-Hduw2sN9XMKcKZQ(jG0$z=#<^>&&|iJ! z5#JX!)wm6}Ynbl`1N8XPori`o$(@YP9u%q|L#}H|q@}7;9-I&r@S$>2_ha2J|1h#+ z;L@-Ewc;>Ap7%U?JlUDqG{ca6;7*p=;&Apd`w>7FhTk*@!a=tuc`AtL$z^y)LqpoT z){O|zT`SxbT~}xlwBJ(-^1YUC0PRVXvgm};=k&ibAH8oN8DGKo0mhKlZAaEq_4*qU zUoUEmYO6VjMJ$&V_c{&OY(3Rt1RnG!7__Uq0GBTcII+LK4LcI4&Jb=q+ z1>?ji0U7wH!Dz7R@jEyu2hz)Sli(lSv=e<4CkZFvx8F#a=;w+OSo@wWmg4Cz8|Eah zT>V{{!s0)@g&e~e-I;Q@tUpr{f^z!lgYg;D2QS}h8e*3J`qi*DSMi__HD%;BdZaR2 zZgYBxO%D*X;ytAbD5qH%|*qPpO$xrMH4 z-ILOHrbSv7apwYmLIWu8K_z(oU2i-wpZE#pZN=^-29-+QqKi5HG0NLfuYi1Pn>Uhw zWZsED<`<>48of;`CvSD_D(48e=6#pZtm9O3s@}T1go8_=~#Aq@3+LV z<*a6v)YbAJ8E_UTadgg3NSg`qm?;rTGeB;}>-^e?R&J0L&-~h%rj(6CDnGe|)(n_x z?D{|C?n2SI;3lD(B$60A3Nc=o zoX|@0b>WS?_8In40I%DXhz2OVpYa+>+O3#m?gpX5=D5>h0|vfgR@&^{A6D8XIs;43^Rt6^FF) z$gRhZ<8;Sl+w~9p-S>?)`yWe zrH3gYVy5b~k5(#%p&!sC77>5QjeZtiVW~^z&eEr3Q-Qm$uAstbC9WnL_QHE`=%Fmz zw@juv(+0r}%KqshoD2T!%}9>&`QEL<)m44M*3n~#?8#LyBYReF4BBRwDl2>FVfrT_ z<1NmhxNrBNHzE6eBR8fjzy4y3nn2%GAv~(a$5+`nHdZ8OzL?b0lTP7t;iLPUGRSF8 z1)g)Wp8H+P!S)A4+xs|twNxL?pIlH{j?B$I(!g5b+0IG`-Ws*FOjl({o|~H4Y}EHW zJ9pe8*A6z$kbLxeB*k3re!Ty}=gbaZP(c;>;L=a2{|Nddjf^;d@jyC%lVV{n!u4=# zPq|VtYcSFJ8Ob~YW0h#6PQTR*La^4&j0im_~cIbJ*jcA8$wvV%s; z07=@L95lzAvL%fo+YC-*`ntMINaB}d%m}W}|8W8c@KVfv&s6DuPUf1Dr*;sPw$STSe&|D2dv}Su z`)!53#G^fG4vV?XC(&*svGMWD&)J_9`H`6oPIp~L*7Mhh7mboQ%%*VUYtrmr{4~`WJGD_zk26%^dN$h%k)3To$|<{W|XSBq%v~OVnoG#NjHM^OMKMxkcC56BXGl|`eERmn*Ho@xk>A?Pg&OzTM%XXY!TK(V5Y#w?hleL^ z#55Kvz|o90#D-i_-)vWrT>a?S&&-kC87S+O5*Fj*U{6-;Nu)=PsTZQvDfiO)Q3E^d z7v{jd5HOW!+sY(1gTO;WL(5_Jd!#>sBLAj0{J=bW2)lE@K6*5{t0Ih=zDy%oUtc#{ z@sjnn(3eIN5)#(lH;;-Y0v<~xBxv1qN$gQTQ^S)>JpgJ5siP=n(>R+_Oo9Re2l~|P z?H##4SwIFxZN`oTyNhj4umpXI@Zxmf5H&S5g&{_HTQ9kV#%%6R+I_vq4Ou95WE~iu zk;J;*xziV(0^kF=z4Dc15a7m!^ATYCes}G?d~X4jLMM-F+%Qp77co1!;zJZZ8Xt9T^FDo9_zvb0~`6i zMQoTjKrrkWIo|=12&#PmD2*H3;r4cR>%a1~r?LY2slbqs`m@}d(pY@R@zTji?}f+p zd;sDcNPF(B59x=X{p|-c^Rwf8DHmQ$)%1uYcwU`*Z*FakTookT1X_1!3_JjOcJ7;h zb#k=0Q{@t8$mv3EHPNTIu~O5K-XuY&{_(s9n^MTr3yezvAE?yf1Fx)FLYn@bCq0%6 z#mm2oi!1Q{zH8O!gUx5T+8is*V~)e!15sTzFgdK-W~|@Sg$iDoJ~!sI9U_~x+S*$4 z0O+mu*R*R=fDHdSiCJ=QVqllvy*sxC1!NUDIc@Y7`|J7`&CprLju@si zk}p%Z;FDRoDI&9#*A`bqV0&>H>#wyT!Y>a~@9B>Hx@4^|EeM0lzS1HG#+)<;ys#ur zV11pQo~}FBN0>ej+UYQJ@1+T%%7WZ)_T-_u;>a1SKm2Bu33$^P7- zjC(-T-TrcC#M08jyYJ8uK~c$Ny>St#fpzjeoDwtB&$}KGXQ2Lga9T`{s3y#-;X&&H zR7IwV`GIxgvx{Tua4x@3bWQH`lS<1pot>RtsWt@=trb{hwo!{jeVXP__jH%Tat11o zZ|o9+_JqO{yX?8$ zW>MX({wBy{Ef{MPvoB#2=1__Sb+RE!AR5H~2*dQ9_EiCQq|Q;HK;&cpH7-uB`CZ;z z4z|Es65+j3ACHJI0wIQ#M!t1XOBl|$uBCWoLVmr%!z|m!A?ES!Kb8PG$+YJ1nN2OK zO-&65>WQ6ke{b^wl1$=-@M5}xGd+XEASB7@?aQOnXVHxp+!pp)(u!Pi#S1+}Zv$3A| zry?vth(?W>v9aUF+~>u-h&L4{V77<>ysLZ)Z`JGCMeBAANIFlrn4G8fqmBZ3OWbi` za#WPJ09gRUq}q5eU(6J9Xoy6%{8>$RYc7CTZ~eyJkI4;+7yTP`lZVR>KqLo+RP7~A zAGDB)f6*?J)vByFu{7_GNFHU`C+Zrb9F#MnX8&E6`zxgv&oEpev!;K1bo5jQN&mjF zp`lp-@oSDg;jFed@`<*zPjT;$XX0kM{h`;z~2RL#pkcLCvkGLj(On$D`W!j$c zp>orIX)nlxYnzL!uo2}Hi4EW_8CMpG(zAOyOlv{>sX8H7^KGxMQ*(U{hcC*@%av?n zn_5|%2i9_uorS__+LnS<1%0GUWsOfb4yv>in`y)2)si2>J_u$Mzr=WMUskl7Zh zDvd8zaL^js+pj%9B;wzQCI^0f9dvKtS&)D*7_-tu#_*F#D$O-S7jq|@lxD8B(o_8# ztU`IoX5T-QBq;>`&Fd(>2|;^??M&CXeSCg49s5O?J2GKuQCpJg=Z|uY&cU((2Sv5E zk*>z*GrkVmPVU!^9~pZi2mI-JmAY)cgdX|Ho8F)ZO`v)2*yb`_>n+C?r(_>{+jRbU z>;m_ERp{!iMs_7y`Ng23U^(I}qb-Bu(?9{z#L^~GY_{<6bB1aRytaJ}bC(76a^Os8 zjg6QI7o+|;v?cT!U#;%B;fRhv<;BIt=^+vog%Q6BSx_|g+SwUbx4pK#M24MWGj`7R zH|cQ7^Cp5xw&sMdx*q5czipW&AN7}Gk7yn!DmhQ%MO^&doxS*yWM4}G1U;h_?{q^T zXnMx93|>kw(bG?l(FWN>5q5X~r`?}^raF5uy*?SD`eoKRYtN}~{}-v%f3x{Kg@Y=$+aN7cUt?4)+%aMM4OvBW1j$mMmIvM z8rk}=;Q$N{1L6UwKkmZXY}jlf0SIsp05h`z;C-WWx+gFIguR1C_d(HsBuYA(41xrz zK=2hoFdR+vAndeJ%k%}=GHmi8A(NlP>vS6ChtX+xAQ+Nt%(b#6=*+41M%Z6b{gqw~ z*_P_*i}h~bbJkpOT#R2;A`^PG$D`8KZO;mZ{a+!j{cpZv`cN>Cq&==d%NKvvQ^?28O6!|Ve@k0ZwH;Kvbhv*ZK`E0$=2zb{OF915?B42_(D7H7xZnOh%gRGwwYxZc4g#D(xwey8bqsSw^=ABS^T^Z%vj%4`nTi|E0XQQC(aEOnwjV4*^oKT zl#e8XfpRTf=Ix$Cn)v5z=R*~y?-gdMleiODtj>h*PhF6JtvwYnk5^J@P|ViO!K zS#qvJ0I_6e;P@FyiE6@DYjpyc!t zVjA6AK^U4-yZm88*|*A{3DHEv)7v~ytJisBrfUVFvIrql9W~tUbkLRTozEyZ} zvGs9OhrnQknvyLjaslz@+Woc^^QSL_uW(wzJU3V9?~YJ0%0PELgfl>pXK(F<2$EO$ z9M5h_1Rdq;qTZKr^%buIaX%(nd8lpd_6>&;BD7Bqb1NwmAe4=%Kg*|HKtS`f#l|yU zL)pcR*1gIqMaj3|YBN92P}us7&Tar=Rrpn|SdIe25}}m-hu3F__ig~ny!!K^UE^d% z>cBk41oL7pngIJ7mZSvnpVPiw0!<%4FuJVB-vniC?hHV?i z0)0hgcC*ioOcya^rN-`eiaGsfSlE!Zlpmc}v@{Ck6YS%sA~)NmbUpPorsci|5-69O zS@Z4T>T&MkdQK5lm)01xd-DbsW#LJytNcug_3yO*nO_b^Lqjlk#ywnNxoy@M=v$go z-WtZ+i^T4`aTS@xw_eeU5v)I?9?)usz;s{n@c39A}L&o6X317M1@z zK@VRRIQN*WNImjU7WgDHm94+PWUMW46idZKF`3A)_H)2p;CStziApl4IQ%3F0Y_iW zIZc0eu?)EiHRX*LDZjGI+HYX|@mC%=x~ zNS4!Xc;PM-{=k}hj|r?7BcoJUsV+$X#Y%YhqH7VbMCB4)AucGqT-XQ|8ccc`g<&j_ zD#MUqocYVEWa{y^?OSCq%XTFbb`N1_K2(}~0zP?+Zfd*`flw4G2A|!){=5ihMYzxGBCKmwODBArZy~5vP=LkFjO5Y6N~yca?(RmRk%q+0e~D|zR%cD z{vXUCYhymdaYPnzHW>dRM2{4*8N?J?n&i_80pKxxxe3GJqenqqHG$65x@#K3*?dG$ zEBU+fvYFnb9I zsfYb~QZklqRoG!S!A*HhPFR!Pd^NjNFsBJTE|-c3WhV9Jt%E|5R3n=c4e@Lu*&phi zKH4%q`OFuKm+1dRD^UGOpzv^Yl{FkqqFvVJAB~RzmSw7LJBy95#eGst!3R-? zuf;BoUAMfGPR-y%Z%O?72j|gKViM+Xt$)g9z!Z?qhX*#ImBl-*?+#P@Zn~3o{UBqi zkeRcP^Iu7EZ1e5iRRG*?!x6o+=|4Ltc{%&Wog{PGaXv$ODGida%i_r+TJt*)n0(!QM_}RKs&Z2o z;bI57tna`6h=Sd z?_G}0V_*VT$!acg=I|Y&fda_bb{M>P>077kK)EQDBbyJs>(uGsa@pp0os#ay3k~XX z1seb;oW>+X7P9$1#_k%e1w`2qToa%FJ0*C-BdTy2_y4>NW z6kDiQ~20S8i_h$&ne=GTTqkeeU)G@mY3)o3uo zki1dC9cJh1LT0L-RV*skf7XtBWIIqpv%01k`TGy7n>&~)uKyo<>QP8*1HVjNP5-R2?y@+QaqwlhV$I?oDis? zK$K7`nvw#I9g2+k+aP~%UJ3+UwXA=Pudn>h#WJE#RQLAJVnBoR)57GA54Sn568#`~ zX`x4W=IJ4c(&b&nsfOzb=f zbLDp8=T6=A0tcpMNfSZ@#P(ce%xtX))BQhzJp+M)E)t@nFn|~PiTo+h|3MV%&xR+hojs58!X@G6J(a( z8Fw{5&N%m^J%5A8N^CNH-!uTM5_?uw(Zr^;W!@qmaX0l7g4=gAFioNOAr zwiGK!6B1kg%_2A+!Tx5-&@v;T+=tqk@JKUe6$CoEef>8mWbllt0?j?gFk>6{!FL!j z+eLnCF%8bodiR2s5Vr|1CWm+^C{$9Pi2ZID1Cz#4FQ=dR)_$Pk zd~Q^y+fptG;?_4<>wCYtApEdD#oA`Z=LyXfyU+NpP>vCsnd(b_S%BkNoEx9KRppWl zTfs*^>wrKIcNr}VZKp)!iFyS*0*0-L!A9Bb`F3$I=sH^m{;iT?*@RtHn|1_nhof{&mKtZ&uiQfE{jA8A@pBApqpVqJS?)7MsL@^L#QvBAc6`2QxWLbj#)K^MV9*jUpet6x`JcBs)SVHb2fsmN2>#!|f z20!=P^Kw)?BXpB>MH+N9^Mj)pSH%>TVNxFosbLxL6-f{>6$vK||4-(q*T~E5{n!4S zHbO3#AS-Vhq!?YU%vG{r8uvg@Y>(>dWYYRa1q=)ybY^?IoJbP@(7p#yz_QjGXFLE5 zfbPtmvY~rK;SX zPg?Tui*e+mG2^>HRbswu44U2#fD+~t({QrMV&$x96M*FFDaBpH;JZ4TKG!5 zwh0?%U?TtzvaNg}0-QS2424Vpuqsqc79iwrwLT{gx&y|CuiOEBnFxJ$Zi)Ui4uN@a z@SP3yB<4S`8eql4l4cOHkAVe>oEOm` zyl`un$4elV{=NwajD#luP(Vb-2{N^PV6pql>GdXMBj8{_3NEQTzm#h7KNC9yy#%gu zC~{s)GfQhWNs8Ejj6nV(Ji!cqEm|wxVmo5v=^K@8?%-?yCnbLo6b#wYEemYX2ehN|PXNu?3G1BHhkr(>3J$-dl@mnDSbAln&Fgc6G{cpWSXv%UFNrkBFPCh$)% zoNst8+5nQ2az-^OZX^9$5#Es<$v2E}q5;7V94z#d+XJAHEUCA5Byc@~IqO>*6>xZC zgTMpTLXGOd7>66#<*?db^zxNFmH~{Bx#UpD9o1qm#Q`7N9q=z$RWz{X(6G9m%auqr zEE}GKMu$O-)DVGC@;f0cf87+4y-G#o33DdbTfGivAw0o_kU+!;2{hK%W-Cb7)4-ntkC-qgq0*E}<3mnLIycUGcX)nbTR2$myi{gO{O zUZ2!fw@Y21El|V#B3Sy8f_Z_rB;OSUL1_R7rk_d=N&jc)AnLR7!5=7qSbs^3dL zSQ$dq8rqORLxrD)n!Jam>>c#IdiTFWEpZsr)WsSVu8pGABorV?SVym zo1gV5=(GG9SbkAxY_NTrcJnoMOpiI0*RAq+Z@NsBx0KFZr81Y%H>Az=U*o`$vE0N# zK6SxL&fRvCeZlRq2irYG3myK~9!Z>)TR+rT(M^7|^I_`cOSw1tx?>BC?>z!?exc*u zM0u%)5sGY^K3N0|DfnIiQ#mnPiBL!Xs~!X5vZU{xVIT^F8}S1#Xx{&T;0+ZzeMxh@ zNvUo09sR7y=j6JqPg#{L0@tX;YlZ5frj;J>HQ!=GvYGWsbTZGX8PC3lU>{^!!? z%Ek}@s{(@j7$<-G>qm<54ZM&6b+sR^mhI2hBF0k$(qWC%U}&OAWMZE(o6$|W-NB|G zQFuElY}@?l3ljg<>XWxIm7ZC_>e;cWazK>?+=>l+Ff_GJeO0klx&4KIe7}!^`ZMdK zEtxI+9T%;MtQbOROe#TtIM9{>z9E}X&`siQS-+Xu#qNc>6@p8@P?0N{{CbpJ(JKqI zsN`q8g$^m5sa6KnTu+t}JoYY9@d*i?Xv&@08}eKUybfI$VEsI-*ckz&$&$awM#8l; z(RKtPClW^mno`+gYiGjedCrhLI^#apnonSD7N|OmI|9)EQ7zRz6Ds}gnTo=4j$cF1 zCf4AZ-V_wsMQQ0S9W4$-)g>yUggxqZ$ick_2eEmtHwM8I3@2DHK_fPs>4LXn0 zfxhUWWA-0Qf;v83Xj>A5XL<+X{oZ-vHLu;v2wEYkBE2}#y#vDr*S}{&&nXgviG#%H z2kC^M*gIg%Z@{eDua-n!KGzo^aIZXZ_qT$c5(;>h^{;W|M@hgYW6YeBX3~H#q}UoH z+5azHQAP=h^$|kR0@^d=knZ@-;8t&3GqFw$7p}-B1bb-MB;AElW6<@Sna-C(B=qJJPhhiEwA=$z8TIdg^U(cnu6{NlY6k8gPW-%I{a0 zk#P7xl(JYN^SIhTP!2dCpsq#hrXj}pVg0el3BA>p)iy%on0mQawF7kg?va50$907~ zf)-;`8WY_%9_TH!gpgM_@UDhm<;84>iEHKbEmFOeB2%xD{H~z14MaP#$`KDb7mXF8 z=r5o9N&3}nZKpD$r~mO999ZNfIur}X&W)e;v8RW<99yv5Z$FtHt3J*JYYVvPr7gyBceu2x3xdHMuM>Vi#k>= zeaNfi{T^bA2|TI%6Apy^jxc1)RVC#tvM6rJ0fm>&UU&9h3V9T3Hbg#D-Q=Fk4x`C5 zB9>Oljz%1w?)ENJ#H@FIJ3|6a?|S!eY$YerdOXi3vDCo-z+~56?=+5Ow|4&KcZO-QuJsT=5o(#fQYhkXQW*v)|Uh=p+hgQ#_Iff{w>N zd{1I)8Hwe-zTVZdpC_8x30vsM7LW(3gMlWpcvXF2NUzhL1$2D%v^M0jvDF@$H!Z_n zEW`O!lzzudSiZ;qkDQeceuf3?AoPkeFL~U=}QfTJ;k%BuZ<61Q{aep8ivG8bSAVWBqowaTM82DbR5P^>0Vm+gmQtVSb&~@ z__~5BS;FgTn#{;{WnxxZ^=&WIs*R`#UYyJ3QXJ>@@t`P>Gn!%^gUo15+!&VZ%ZB~W z9A4zUEVF&|6xfzVw$ld3k}65Om?%RcTSMV6i;icZ9gcMkEQ>Usc?uy(O+?VQTx-?6 z1vGId_7s@k%kS^A9w}nT&A#{X7rLxN1o~nMvZVtuy9z2gp+lx2u^yGZJ>gplj17+6 zVxW@n(ISU;d8|I)cZDn#z+^v5$iN5d5;R5L%kv$BUre*#*xys-f#fM-APzGJymlf?=E^9-b)Pq{kJ%Q~B**J957@yP^B9;7?avzU~Hww!=>9+stvcX-`o1?w5acbph#UFN%y1+da7G~%- zS)oO3620#tyz~8OyV5ra%R|niUFwp9*1Gy39|_?^FI)cTlrR06QF8yh^qIuRi#+TTCdwP2ogS>jXKN{z3jPNT8yP$Afa$85UVC7Yy z7FU75bxzZf%9)ALh}-P!@^+oEhW9to`}Mk}mkg53HGiu1(}>Ox#YvCCb-JNHnL4n8~Xt z)v)3P8qn{a*jI_s%-jr5c;ZvjTVnTsxLtSIIn_BfDzNPNGkO6}x;A$9SrHav&5rL> z>OPjf37z=OFg^d>7wT?yIAe~&k%ofArN)3Qhdu85J63#+i02Iz(tLqJh%%l3uS4IZ zrB<%tv;+_Fo^p(BFAt|=(^Au^g+q+wNmv^l-=LlMGu$L9XzauMUvi&*A@xG3BD_DV zlR}*>KV$)hIZcc#&Gb@G?+;m{sn>rS5KD#!?db+}r6#k@W&aHshS+2E5#VtSL31jd z#AqK1?GSXo2I;z~Ck1Xg2cVkCFdIfYYTSo7pM)v8=HbPl-2F19Y?tZGw4Ha)%Q||D zUbDJe7kqT8|BhSk&OUkiJT>>8=+bjRC9byE;|%;sd)*-Uh&VsG&X@Q5e%s2Lh@!(TvEQB$Ep&g$j=0sK`DasU7T literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png b/app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png new file mode 100644 index 0000000000000000000000000000000000000000..127beca923203880912c1152b5423cedbed27d0f GIT binary patch literal 7859 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 XP7XdhQy2&iqiGOVwN*&48xQ{r$t{cH literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/splash.png b/app/data/ct.libs/inherit/InheritanceTest/img/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..127beca923203880912c1152b5423cedbed27d0f GIT binary patch literal 7859 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 XP7XdhQy2&iqiGOVwN*&48xQ{r$t{cH literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/README.md b/app/data/ct.libs/inherit/README.md new file mode 100644 index 000000000..83a2c43b9 --- /dev/null +++ b/app/data/ct.libs/inherit/README.md @@ -0,0 +1,38 @@ +This module implements type inheritance for your types! Firstly, enable the module, then select the parent for one of your types. Then, use one of these functions to call the parent's logic: + +* `this.inherit.onCreate();` +* `this.inherit.onStep();` +* `this.inherit.onDraw();` +* `this.inherit.onDestroy();` + +And that's it! + +## Checking whether a copy is a child of a particular type + +There are two methods that are the opposites of each other: + +* `ct.inherit.isChild(copy, assertedParent)`, and +* `ct.inherit.isParent(copy, assertedChild)`. + +The arguments can be either copies or the names of types, or a mix of both. So you can check `ct.inherit.isChild(this, 'Godwoken')` and get a proper result. + +> **Note:** if the two arguments belong to one type, both methods will return `true`. + +## Getting an array of all the copies of a particular parent + +You can get an array of all the copies of a particular type and its children with `ct.inherit.list('typeName')`; + +```js +const monsters = ct.inherit.list('GenericMonster'); +for (const monster of monsters) { + // Let the bloody massacre begin! + monster.kill = true; +} +``` + +> **Warning:** contrary to `ct.types.list['typeName']`, the returned array is not updated and will be a source of memory leaks with deleted copies if not handled properly. Do store the returned value in `var`, `let` or `const`. + +## Things to watch out + +* Events are not inherited by default, so you should explicitly write `this.inherit.onCreate` and other methods where you need them. This is due to the fact that in ct.js v1 each event is still a function, even though an empty one. This behavior will probably be changed in v2. +* Don't make circular references when you form a parenting chain where one child becomes a child of another. They won't work anyways. If you get an error `Maximum call stack size exceeded`, then you do have circular references. \ No newline at end of file diff --git a/app/data/ct.libs/inherit/index.js b/app/data/ct.libs/inherit/index.js new file mode 100644 index 000000000..afa3ff0f0 --- /dev/null +++ b/app/data/ct.libs/inherit/index.js @@ -0,0 +1,50 @@ +ct.inherit = { + isChild(type, assertedType) { + // Get type names from copies + if (type instanceof Copy) { + ({type} = type); + } + if (assertedType instanceof Copy) { + assertedType = assertedType.type; + } + // Throw an error if a particular type does not exist. + if (!(type in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${type} does not exist. A typo?`); + } + if (!(assertedType in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${assertedType} does not exist. A typo?`); + } + // Well, technically a type is not a child of itself, but I suppose you expect to get `true` + // while checking whether a copy belongs to a particular class. + if (type === assertedType) { + return true; + } + let proposedType = ct.types.templates[type].extends.inheritedType; + while (proposedType) { + if (proposedType === assertedType) { + return true; + } + proposedType = ct.types.templates[proposedType].extends.inheritedType; + } + return false; + }, + isParent(type, assertedType) { + return ct.inherit.isChild(assertedType, type); + }, + list(type) { + // Throw an error if a particular type does not exist. + if (!(type in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${type} does not exist. A typo?`); + } + + // Get a list of all child types to concat their ct.types.lists in one go + const types = []; + for (const i in ct.types.list) { + if (i !== 'BACKGROUND' && i !== 'TILELAYER' && ct.inherit.isParent(type, i)) { + types.push(i); + } + } + + return [].concat(...types.map(t => ct.types.list[t])); + } +}; diff --git a/app/data/ct.libs/inherit/injects/onbeforecreate.js b/app/data/ct.libs/inherit/injects/onbeforecreate.js new file mode 100644 index 000000000..951e0ca63 --- /dev/null +++ b/app/data/ct.libs/inherit/injects/onbeforecreate.js @@ -0,0 +1,40 @@ +if ((this instanceof ct.types.Copy) && this.inheritedType) { + this.inherit = { + onCreate: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onCreate.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onStep: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onStep.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onDraw: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onDraw.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onDestroy: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onDestroy.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + } + }; +} diff --git a/app/data/ct.libs/inherit/module.json b/app/data/ct.libs/inherit/module.json new file mode 100644 index 000000000..ea2151f50 --- /dev/null +++ b/app/data/ct.libs/inherit/module.json @@ -0,0 +1,15 @@ +{ + "main": { + "name": "Type inheritance", + "version": "1.0.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }] + }, + "typeExtends": [{ + "name": "Parent", + "type": "type", + "key": "inheritedType@@type" + }] +} diff --git a/app/data/ct.libs/inherit/types.d.ts b/app/data/ct.libs/inherit/types.d.ts new file mode 100644 index 000000000..034a54449 --- /dev/null +++ b/app/data/ct.libs/inherit/types.d.ts @@ -0,0 +1,75 @@ +declare namespace ct { + /** + * A module that brings inheritance capabilities to your copies. + */ + namespace inherit { + + /** + * Checks whether a first given entity is a child of another. + * You can check one copy against another, or a copy against a type name (a string), + * or the reverse, or even use type names only, meaning that `ct.inherit.isChild(this, 'Godwoken')` + * will work and will return the expected result. + * + * @param {Copy|string} copy The copy to check against, or the name of a type + * @param {Copy|string} assertedParent The copy that may be the parent of `copy`, or the name of a type. + * + * @returns `true` if the type of a first entity is a child type of a second one, + * or if this is one type. Returns `false` otherwise. If you don't want copies + * of one type to match, compare their `type` parameters mnually prior to using `isChild`. + */ + function isChild(copy: Copy | string, assertedParent: Copy | string): boolean; + + /** + * Checks whether a first given entity is a parent of another. + * You can check one copy against another, or a copy against a type name (a string), + * or the reverse, or even use type names only, meaning that `ct.inherit.isParent('AbstractMonster', this)` + * will work and will return the expected result. + * + * @param {Copy|string} copy The copy to check against, or the name of a type + * @param {Copy|string} assertedChild The copy that may be the child of `copy`, or the name of a type. + * + * @returns `true` if the type of a first entity is a parent type of a second one, + * or if this is one type. Returns `false` otherwise. If you don't want copies + * of one type to match, compare their `type` parameters mnually prior to using `isParent`. + */ + function isParent(copy: Copy | string, assertedChild: Copy | string): boolean; + + /** + * Returns an array of all the copies of a particular type and its children. + */ + function list(type: string): Array; + } +} + + +interface Copy { + /** + * This module implements type inheritance for your types. + */ + inherit: { + /** + * Calls the onCreate method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onCreate(): void; + /** + * Calls the onStep method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onStep(): void; + /** + * Calls the onDraw method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onDraw(): void; + /** + * Calls the onDestroy method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onDestroy(): void; + } +} \ No newline at end of file diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 457bfb795..607716bee 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -8,31 +8,33 @@ }] }, "fields": [{ + "name": "Partitioning", + "type": "h2" + }, { "name": "Grid size X", "help": "Tells ct.place how to spacially group copies. This should be at least as large as the horizontal side of the biggest colliding sprite of your game.", "key": "gridX", - "id": "gridX", "default": 512, "type": "number" }, { "name": "Grid size Y", "help": "Tells ct.place how to spacially group copies. This should be at least as large as the vertical size of the biggest colliding sprite of your game.", "key": "gridY", - "id": "gridY", "default": 512, "type": "number" }, { "name": "Debug mode", + "type": "h2" + }, { + "name": "Enable", "help": "Displays collision shapes, collision groups and partitions. It will also write additional keys to most colliding objects. Doesn't work on hidden objects.", "key": "debugMode", - "id": "debugMode", "default": false, "type": "checkbox" }, { "name": "Debug text size", "help": "", "key": "debugText", - "id": "debugText", "default": 16, "type": "number" }], diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index e5cfbd254..b3697fc7e 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -10,6 +10,7 @@ "apply": "Apply", "cancel": "Cancel", "cannotBeEmpty": "This cannot be empty", + "clear": "Clear", "confirmDelete": "Are you sure you want to delete {0}? It cannot be undone.", "contribute": "Contribute", "copy": "Copy", @@ -177,7 +178,7 @@ "openSpaceShooterTutorial": "Learn how to make a space shooter", "openPlatformerTutorial": "Learn how to make a platformer", "openJettyCatTutorial": "Learn how to make a Jetty Cat", - "doNothing": "Skip this screen", + "doNothing": "Skip this screen and make a great game!", "showOnboardingCheckbox": "Show this screen when creating a new project" }, "settings": { diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 71bd7bad6..ca0edc8d0 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -10,6 +10,7 @@ "apply": "Применить", "cancel": "Отмена", "cannotBeEmpty": "Не может быть пустым", + "clear": "Очистить", "confirmDelete": "Вы уверены, что хотите удалить {0}? Отменить удаление будет невозможно!", "contribute": "Внести вклад в разработку", "copy": "Копировать", diff --git a/app/package-lock.json b/app/package-lock.json index dcf476895..1e99c58b4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2639,9 +2639,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.defaults": { "version": "4.2.0", diff --git a/package-lock.json b/package-lock.json index a5a3913fa..ec32c2bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,7 +769,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -4262,7 +4263,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "micromatch": { "version": "3.1.10", @@ -6114,9 +6116,9 @@ "integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==" }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._baseassign": { "version": "3.2.0", @@ -8737,7 +8739,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index d5776bf37..8ba9c287f 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -1,4 +1,5 @@ const glob = require('./../glob'); +const {getUnwrappedExtends} = require('./utils'); const getStartingRoom = proj => { let [startroom] = proj.rooms; // picks the first room by default @@ -35,7 +36,7 @@ const stringifyRooms = proj => { const layer = { depth: tileLayer.depth, tiles: [], - extends: tileLayer.extends + extends: tileLayer.extends ? getUnwrappedExtends(tileLayer.extends) : {} }; for (const tile of tileLayer.tiles) { for (let x = 0; x < tile.grid[2]; x++) { diff --git a/src/node_requires/exporter/types.js b/src/node_requires/exporter/types.js index 493004709..e9048e1cf 100644 --- a/src/node_requires/exporter/types.js +++ b/src/node_requires/exporter/types.js @@ -1,4 +1,5 @@ -const textures = require('../resources/textures'); +const {getTextureFromId} = require('../resources/textures'); +const {getUnwrappedExtends} = require('./utils'); const stringifyTypes = function (proj) { /* Stringify types */ @@ -8,7 +9,7 @@ const stringifyTypes = function (proj) { types += ` ct.types.templates["${type.name}"] = { depth: ${type.depth}, - ${type.texture !== -1 ? 'texture: "' + textures.getTextureFromId(type.texture).name + '",' : ''} + ${type.texture !== -1 ? 'texture: "' + getTextureFromId(type.texture).name + '",' : ''} onStep: function () { ${type.onstep} }, @@ -21,7 +22,7 @@ ct.types.templates["${type.name}"] = { onCreate: function () { ${type.oncreate} }, - extends: ${JSON.stringify(type.extends || {})} + extends: ${type.extends ? JSON.stringify(getUnwrappedExtends(type.extends), null, 4) : '{}'} }; ct.types.list['${type.name}'] = [];`; } diff --git a/src/node_requires/exporter/utils.js b/src/node_requires/exporter/utils.js new file mode 100644 index 000000000..5beecce5f --- /dev/null +++ b/src/node_requires/exporter/utils.js @@ -0,0 +1,51 @@ +/** + * Creates a copy of an extends object, turning UIDs of resources into the names of these resources. + * Understands notations of `name@@type` and `name@@texture`. + * + * @param {object} exts A flat map of extends. + * + * @returns {object} An object with unwrapped extends. + */ +const {getTypeFromId} = require('./../resources/types'); +const {getTextureFromId} = require('./../resources/textures'); +const getUnwrappedExtends = function getUnwrappedExtends(exts) { + const out = {}; + for (const i in exts) { + const split = i.split('@@'); + if (split.length === 1) { + out[i] = exts[i]; + continue; + } + const postfix = split.pop(); + const key = split.join('@@'); + if (postfix === 'type') { + try { + out[key] = getTypeFromId(exts[i]).name; + } catch (e) { + alertify.error(`Could not resolve UID ${exts[i]} for field ${key} as a type. Returning -1.`); + console.error(e); + // eslint-disable-next-line no-console + console.trace(); + out[key] = -1; + } + } else if (postfix === 'texture') { + try { + out[key] = getTextureFromId(exts[i]).name; + } catch (e) { + alertify.error(`Could not resolve UID ${exts[i]} for field ${key} as a texture. Returning -1.`); + console.error(e); + // eslint-disable-next-line no-console + console.trace(); + out[key] = -1; + } + } else { + // Seems to be an unsupported postfix. Output the old key as is. + out[i] = exts[i]; + } + } + return out; +}; + +module.exports = { + getUnwrappedExtends +}; diff --git a/src/node_requires/resources/types/defaultType.js b/src/node_requires/resources/types/defaultType.js index 7a8be4b15..e7fc9e4f3 100644 --- a/src/node_requires/resources/types/defaultType.js +++ b/src/node_requires/resources/types/defaultType.js @@ -15,6 +15,7 @@ module.exports = { get() { return ({ ...defaultTypeTemplate, + extends: {}, uid: generateGUID() }); } diff --git a/src/node_requires/resources/types/index.js b/src/node_requires/resources/types/index.js index e53ebb289..11e394066 100644 --- a/src/node_requires/resources/types/index.js +++ b/src/node_requires/resources/types/index.js @@ -1,6 +1,6 @@ const getDefaultType = require('./defaultType').get; -const createNewType = function (name) { +const createNewType = function createNewType(name) { const type = getDefaultType(); if (name) { type.name = String(name); @@ -10,7 +10,40 @@ const createNewType = function (name) { return type; }; +/** + * Gets the ct.js type object by its id. + * @param {string} id The id of the ct.js type + * @returns {IType} The ct.js type object + */ +const getTypeFromId = function getTypeFromId(id) { + const type = global.currentProject.types.find(t => t.uid === id); + if (!type) { + throw new Error(`Attempt to get a non-existent type with ID ${id}`); + } + return type; +}; + +/** + * Retrieves the full path to a thumbnail of a given type. + * @param {string|IType} type Either the id of the type, or its ct.js object + * @param {boolean} [x2] If set to true, returns a 128x128 image instead of 64x64. + * @param {boolean} [fs] If set to true, returns a file system path, not a URI. + * @returns {string} The full path to the thumbnail. + */ +const getTypePreview = function getTypePreview(type, x2, fs) { + const {getTexturePreview} = require('./../textures'); + if (typeof type === 'string') { + type = getTypeFromId(type); + } + if (type === -1) { + return getTexturePreview(-1, x2, fs); + } + return getTexturePreview(type.texture, x2, fs); +}; + module.exports = { getDefaultType, + getTypeFromId, + getTypePreview, createNewType }; diff --git a/src/riotTags/modules-panel.tag b/src/riotTags/modules-panel.tag index 6d793dc61..208dbdff8 100644 --- a/src/riotTags/modules-panel.tag +++ b/src/riotTags/modules-panel.tag @@ -90,48 +90,7 @@ modules-panel.panel.view code {currentModuleLicense} #modulesettings.tabbed.nbt(show="{tab === 'modulesettings'}" if="{currentModule.fields && currentModuleName in global.currentProject.libs}") - dl(each="{field in currentModule.fields}") - dt - label.block.checkbox(if="{field.type === 'checkbox'}") - input( - type="checkbox" - checked="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - | {field.name} - span(if="{field.type !== 'checkbox'}") - | {field.name} - dd - textarea( - if="{field.type === 'textfield'}" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - input( - if="{field.type === 'number'}" - type="number" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - label.block.checkbox(if="{field.type === 'radio'}" each="{option in field.options}") - input( - type="radio" - value="{option.value}" - checked="{global.currentProject.libs[currentModuleName][field.id] === option.value}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - | {option.name} - div(class="desc" if="{option.help}") - raw(ref="raw" content="{md.render(option.help)}") - input( - if="{['checkbox', 'number', 'textfield', 'radio'].indexOf(field.type) === -1}" - type="text" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - //- That's a bad idea!!! - div(class="desc" if="{field.help}") - raw(ref="raw" content="{md.render(field.help)}") + extensions-editor(customextends="{currentModule.fields}" entity="{global.currentProject.libs[currentModuleName]}") #modulehelp.tabbed.nbt(show="{tab === 'modulehelp'}" if="{currentModuleDocs}") raw(ref="raw" content="{currentModuleDocs}") #modulelogs.tabbed.nbt(show="{tab === 'modulelogs'}" if="{currentModuleLogs}") diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag index 21f992d08..c50e87f73 100644 --- a/src/riotTags/project-settings/project-settings.tag +++ b/src/riotTags/project-settings/project-settings.tag @@ -1,6 +1,6 @@ project-settings.panel.view.pad.flexrow - - var tabs = ['authoring', 'actions', 'branding', 'rendering', 'scripts']; + var tabs = ['authoring', 'actions', 'branding', 'scripts', 'rendering']; var iconMap = { authoring: 'edit', actions: 'airplay', diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index 20770ec13..e1a3ef2f9 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -28,7 +28,7 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix svg.feather use(xlink:href="data/icons.svg#plus") .block - extensions-editor(type="tileLayer" entity="{parent.currentTileLayer.extends}" compact="yep") + extensions-editor(type="tileLayer" entity="{parent.currentTileLayer.extends}" compact="yep" wide="sure") texture-selector(ref="tilesetPicker" if="{pickingTileset}" oncancelled="{onTilesetCancel}" onselected="{onTilesetSelected}") script. this.parent.tileX = 0; diff --git a/src/riotTags/shared/asset-viewer.tag b/src/riotTags/shared/asset-viewer.tag index 262ab2f00..8c55ceedf 100644 --- a/src/riotTags/shared/asset-viewer.tag +++ b/src/riotTags/shared/asset-viewer.tag @@ -3,8 +3,10 @@ @slot Can use nested tags. Yields the passed markup as a header of an asset viewer. + @attribute class (string) This tag has its own CSS classes, but allows arbitrary ones added as an attribute. + @attribute namespace (string) A unique namespace used to store settings. Fallbacks to 'default'. @attribute vocspace (string) @@ -124,4 +126,6 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") this.switchLayout = () => { const key = this.opts.namespace ? (this.opts.namespace + 'Layout') : 'defaultAssetLayout'; localStorage[key] = localStorage[key] === 'list' ? 'grid' : 'list'; - }; \ No newline at end of file + }; + + this.updateList(); \ No newline at end of file diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index b58928314..ff08ecafe 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -5,45 +5,103 @@ @attribute entity (riot object) An object to which apply editing to. @attribute type (string, 'type'|'tileLayer') - The type of the edited asset. + The type of the edited asset. Not needed if customextends is set. @attribute [compact] (atomic) Whether to use a more compact layout, replacing full-text hints with icons and using more compact classes for fields. + @attribute [wide] (atomic) + Whether to prefer a full-width layout. Useful for making neat columns of editable fields. @attribute [customextends] (riot Array) Instead of reading modules' directory, use these extends specification instead. Useful for quickly generating markup for built-in fields. + Extensions are an array of objects. The format of an extension is as following: + + declare interface IExtensionField { + name: string, // the displayed name. + // Below 'h1', 'h2', 'h3', 'h4' are purely decorational, for grouping fields. Others denote the type of an input field. + type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textbox' | 'number' | 'checkbox' | 'radio' | 'texture' | 'type', + key?: string, // the name of a JSON key to write into the `opts.entity`. Not needed for hN types, but required otherwise + // The key may have special suffixes that tell the exporter to unwrap foreign keys (resources' UIDs) into asset names. + // These are supposed to always be used with `'type'` and `'texture'` input types. + // Example: 'enemyClass@@type', 'background@@texture'. + default?: any, // the default value; it is not written to the `opts.entity`, but is shown in inputs. + help?: string, // a text label describing the purpose of a field + options?: Array<{ // Used with type === 'radio'. + value: any, + name: string, + help?: string + }>, + collect?: boolean, // Whether to collect values and suggest them later as an auto-completion results. (Not yet implemented) + collectScope?: string // The name of a category under which to store suggestions from `collect`. + } + extensions-editor virtual(each="{ext in extensions}") - label - div(class="{parent.opts.compact ? 'flexrow' : 'block'}") - input.nogrow( - type="checkbox" + h1(if="{ext.type === 'h1'}") {ext.name} + h2(if="{ext.type === 'h2'}") {ext.name} + h3(if="{ext.type === 'h3'}") {ext.name} + h4(if="{ext.type === 'h4'}") {ext.name} + dl(class="{compact: compact}" if="{['h1', 'h2', 'h3', 'h4'].indexOf(ext.type) === -1}") + dt + label.block.checkbox(if="{ext.type === 'checkbox'}") + input.nogrow( + if="{ext.type === 'checkbox'}" + type="checkbox" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + span {ext.name} + hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") + span(if="{ext.type !== 'checkbox'}") + b {ext.name} + b : + hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") + dd + texture-input( + if="{ext.type === 'texture'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + val="{parent.opts.entity[ext.key] || ext.default}" + onselected="{writeUid(ext.key)}" + ) + type-input( + if="{ext.type === 'type'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + val="{parent.opts.entity[ext.key] || ext.default}" + onselected="{writeUid(ext.key)}" + ) + input( + if="{ext.type === 'text'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + type="text" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + textarea( + if="{ext.type === 'textfield'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + input( + if="{ext.type === 'number'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + type="number" value="{parent.opts.entity[ext.key] || ext.default}" onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'checkbox'}" ) - b.nogrow {ext.name} - b.nogrow(if="{ext.type !== 'checkbox'}") : - .filler(if="{parent.opts.compact}") - hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") - input.wide( - class="{compact: parent.opts.compact}" - type="text" - value="{parent.opts.entity[ext.key] || ext.default}" - onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'text'}" - ) - input.wide( - class="{compact: parent.opts.compact}" - type="number" - value="{parent.opts.entity[ext.key] || ext.default}" - onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'number'}" - ) - .dim(if="{ext.help && !parent.opts.compact}") {ext.help} + label.block.checkbox(if="{ext.type === 'radio'}" each="{option in ext.options}") + input( + type="radio" + value="{option.value}" + checked="{parent.parent.opts.entity[ext.key] === option.value}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + | {option.name} + div.desc(if="{option.help}") {option.help} + .dim(if="{ext.help && !parent.opts.compact}") {ext.help} script. const libsDir = './data/ct.libs'; const fs = require('fs-extra'), @@ -71,6 +129,26 @@ extensions-editor }); } }; + + this.on('update', () => { + if (!this.opts.entity) { + console.error('extension-editor tag did not receive its `entity` object for editing!'); + console.log(this); + } + if (this.opts.customextends && this.opts.customextends !== this.extensions) { + this.extensions = this.opts.customextends; + } + }); + + this.writeUid = field => obj => { + if (obj) { + this.opts.entity[field] = obj.uid; + } else { + this.opts.entity[field] = -1; + } + this.update(); + }; + window.signals.on('modulesChanged', this.refreshExtends); this.on('unmount', () => { window.signals.off('modulesChanged', this.refreshExtends); diff --git a/src/riotTags/shared/type-input.tag b/src/riotTags/shared/type-input.tag new file mode 100644 index 000000000..ffc01846c --- /dev/null +++ b/src/riotTags/shared/type-input.tag @@ -0,0 +1,66 @@ +// + A button that allows to pick a ct.js type, showing current selection's miniature. + + @attribute showempty (any string or empty) + If set, allows to pick no type. + @attribute val (type's uid or -1) + Current input's value + @attribute header (string) + Passed to the type selector, showing a header in the top-left corner. + @attribute onselected (riot function) + A callback that is called when a type is selected, or when no type was selected. + Passes the type's object and its ID as two arguments. +type-input + .flexrow + img(src="{getTypePreview(val || -1)}" onclick="{openSelector}") + input.wide( + type="text" readonly + value="{val && val !== -1 ? getTypeFromId(val).name : voc.select}" + onclick="{openSelector}" + ) + .spacer(if="{val && val !== -1}") + button.nmr.square.inline(if="{val && val !== -1}" title="{voc.clear}" onclick="{clearInput}") + svg.feather + use(xlink:href="data/icons.svg#x") + type-selector( + if="{selectingType}" + onselected="{onSelected}" + oncancelled="{onCancelled}" + header="{opts.header}" + ) + script. + this.namespace = 'common'; + this.mixin(window.riotVoc); + + const {getTypePreview, getTypeFromId} = require('./data/node_requires/resources/types'); + this.getTypePreview = getTypePreview; + this.getTypeFromId = getTypeFromId; + + this.val = this.opts.val || -1; + this.openSelector = () => { + this.selectingType = true; + }; + this.onSelected = type => () => { + if (this.opts.onselected) { + this.opts.onselected(type, type.uid); + } + this.val = type.uid; + this.selectingType = false; + this.update(); + }; + this.clearInput = () => { + if (this.opts.onselected) { + this.opts.onselected(null, -1); + } + this.val = -1; + }; + this.onCancelled = () => { + this.selectingType = false; + this.update(); + }; + + this.on('update', () => { + if (this.val !== this.opts.val) { + this.val = this.opts.val; + } + }); \ No newline at end of file diff --git a/src/riotTags/shared/type-selector.tag b/src/riotTags/shared/type-selector.tag new file mode 100644 index 000000000..7ff6a4156 --- /dev/null +++ b/src/riotTags/shared/type-selector.tag @@ -0,0 +1,26 @@ +// + Allows users to pick a type object. + + @attribute header (any string or empty) + An optional header shown in the top-left corner + @attribute onselected (riot function) + A two-fold function (type => e => {…}). Calls the funtion with the selected + ct type as the only argument in the first function, and MouseEvent in the second. + @attribute oncancelled (riot function) + Calls the funtion when a user presses the "Cancel" button. Passes no arguments. + +type-selector.panel.view.flexfix + .flexfix-body + asset-viewer( + collection="{global.currentProject.types}" + namespace="types" + click="{opts.onselected}" + thumbnails="{thumbnails}" + ref="types" + class="tall" + ) + h1(if="{opts.header}") {opts.header} + .flexfix-footer(if="{opts.oncancelled}") + button(onclick="{opts.oncancelled}") {window.languageJSON.common.cancel} + script. + this.thumbnails = require('./data/node_requires/resources/types').getTypePreview; diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 91d6d3307..2ae3c6a3e 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -12,7 +12,7 @@ type-editor.panel.view.flexrow b {voc.depth} input.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") .flexfix-body - extensions-editor(type="type" entity="{type.extends}") + extensions-editor(type="type" entity="{type.extends}" wide="yep" compact="probably") br br docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") diff --git a/src/riotTags/types-panel.tag b/src/riotTags/types-panel.tag index 1702a89ac..d886d7129 100644 --- a/src/riotTags/types-panel.tag +++ b/src/riotTags/types-panel.tag @@ -24,9 +24,7 @@ types-panel.panel.view this.sort = 'name'; this.sortReverse = false; - this.thumbnails = type => (type.texture !== -1 ? - `${glob.texturemap[type.texture].src.split('?')[0]}_prev.png?cache=${this.getTypeTextureRevision(type)}` : - 'data/img/notexture.png'); + this.thumbnails = require('./data/node_requires/resources/types').getTypePreview; this.setUpPanel = () => { this.fillTypeMap(); diff --git a/src/styl/tags/shared/extensions-editor.styl b/src/styl/tags/shared/extensions-editor.styl new file mode 100644 index 000000000..f390c8072 --- /dev/null +++ b/src/styl/tags/shared/extensions-editor.styl @@ -0,0 +1,5 @@ +extensions-editor + type-selector.view, texture-selector.view + position fixed + z-index 9 + cursor default \ No newline at end of file diff --git a/src/styl/tags/shared/texture-selector.styl b/src/styl/tags/shared/texture-selector.styl new file mode 100644 index 000000000..26e73fb46 --- /dev/null +++ b/src/styl/tags/shared/texture-selector.styl @@ -0,0 +1,4 @@ +texture-selector + z-index 4 + position fixed !important + padding 1em \ No newline at end of file diff --git a/src/styl/tags/shared/type-input.styl b/src/styl/tags/shared/type-input.styl new file mode 100644 index 000000000..410ce1558 --- /dev/null +++ b/src/styl/tags/shared/type-input.styl @@ -0,0 +1,5 @@ +type-input > .flexrow > img + width 2rem + height @width + align-self center + margin-right 0.5rem \ No newline at end of file diff --git a/src/styl/tags/shared/type-selector.styl b/src/styl/tags/shared/type-selector.styl new file mode 100644 index 000000000..601a394c5 --- /dev/null +++ b/src/styl/tags/shared/type-selector.styl @@ -0,0 +1,2 @@ +type-selector + @extends texture-selector \ No newline at end of file diff --git a/src/styl/tags/textures/texture-selector.styl b/src/styl/tags/textures/texture-selector.styl deleted file mode 100644 index e86b75909..000000000 --- a/src/styl/tags/textures/texture-selector.styl +++ /dev/null @@ -1,2 +0,0 @@ -texture-selector - position fixed !important \ No newline at end of file diff --git a/src/styl/tags/textures/textures-panel.styl b/src/styl/tags/textures/textures-panel.styl index 3054ca09c..d602d4ba6 100644 --- a/src/styl/tags/textures/textures-panel.styl +++ b/src/styl/tags/textures/textures-panel.styl @@ -1,5 +1,2 @@ -textures-panel, texture-selector - padding 1em - -texture-selector - z-index 4 +textures-panel + padding 1em \ No newline at end of file From 94af9b3738cee8f3a905329de5b8e8ed0f679ab0 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 15:55:50 +1200 Subject: [PATCH 45/86] :sparkles: Add point2D input for modules' settings and injections --- src/riotTags/shared/extensions-editor.tag | 20 ++++++++++++++++++++ src/styl/inputs.styl | 21 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index ff08ecafe..30717d8e5 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -66,6 +66,26 @@ extensions-editor val="{parent.opts.entity[ext.key] || ext.default}" onselected="{writeUid(ext.key)}" ) + .aPoint2DInput(if="{ext.type === 'point2D'}" class="{compact: parent.opts.compact, wide: parent.opts.wide}") + label + span X: + input( + class="{compact: parent.opts.compact}" + type="number" + step="8" + value="{parent.opts.entity[ext.key]? parent.opts.entity[ext.key][0] : ext.default[0]}" + onchange="{wire('this.opts.entity.'+ ext.key + '.0')}" + ) + .spacer + label + span.nogrow Y: + input( + class="{compact: parent.opts.compact}" + type="number" + step="8" + value="{parent.opts.entity[ext.key]? parent.opts.entity[ext.key][1] : ext.default[1]}" + onchange="{wire('this.opts.entity.'+ ext.key + '.1')}" + ) type-input( if="{ext.type === 'type'}" class="{compact: parent.opts.compact, wide: parent.opts.wide}" diff --git a/src/styl/inputs.styl b/src/styl/inputs.styl index d4c3596bb..fb5801291 100644 --- a/src/styl/inputs.styl +++ b/src/styl/inputs.styl @@ -452,4 +452,23 @@ input[type="range"] width 100% border-right 0 border-left 0 - cursor ns-resize \ No newline at end of file + cursor ns-resize + +.aPoint2DInput + display flex + flex-flow row nowrap + width 20rem + max-width 100% + label + flex 1 1 auto + input + max-width 100% + width calc(100% - 3rem) + &.compact + width 15rem + &.wide + width 100% + span + margin-right 0.5rem + .spacer + flex 0 0 auto \ No newline at end of file From 809e79694d4f421743f09397be167fc708c404d1 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 15:56:22 +1200 Subject: [PATCH 46/86] :zap: Move depth input at type-editor into a scrollbox, on par with module provided fields --- src/riotTags/type-editor.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 2ae3c6a3e..bd648d3db 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -9,9 +9,9 @@ type-editor.panel.view.flexrow input.wide(type="text" onchange="{wire('this.type.name')}" value="{type.name}") .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} br + .flexfix-body b {voc.depth} input.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") - .flexfix-body extensions-editor(type="type" entity="{type.extends}" wide="yep" compact="probably") br br From efe09f60d3ee56e872833ed54b2a3054e4296630 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 15:57:52 +1200 Subject: [PATCH 47/86] :pencil2: Document point2D type at extensions-editor --- src/riotTags/shared/extensions-editor.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index 30717d8e5..c6bce8654 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -22,7 +22,7 @@ declare interface IExtensionField { name: string, // the displayed name. // Below 'h1', 'h2', 'h3', 'h4' are purely decorational, for grouping fields. Others denote the type of an input field. - type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textbox' | 'number' | 'checkbox' | 'radio' | 'texture' | 'type', + type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textbox' | 'number' | 'point2D' | 'checkbox' | 'radio' | 'texture' | 'type', key?: string, // the name of a JSON key to write into the `opts.entity`. Not needed for hN types, but required otherwise // The key may have special suffixes that tell the exporter to unwrap foreign keys (resources' UIDs) into asset names. // These are supposed to always be used with `'type'` and `'texture'` input types. From 11fe559f3a4a2ea121c305926edbe17a4e50edca Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 31 Jul 2020 10:22:09 +1200 Subject: [PATCH 48/86] :zap: Remove empty "help" field from ct.place > module.json --- app/data/ct.libs/place/module.json | 1 - 1 file changed, 1 deletion(-) diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 607716bee..67c91e7c4 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -33,7 +33,6 @@ "type": "checkbox" }, { "name": "Debug text size", - "help": "", "key": "debugText", "default": 16, "type": "number" From 9705517a96f6b70bd643d1ebb38c443dc5f73380 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 31 Jul 2020 11:19:27 +1200 Subject: [PATCH 49/86] :bug: Fix missing `extends` field in tile layers --- src/riotTags/rooms/room-tile-editor.tag | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index e1a3ef2f9..6981cee1f 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -38,7 +38,8 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix if (!('tiles' in this.opts.room) || !this.opts.room.tiles.length) { this.opts.room.tiles = [{ depth: -10, - tiles: [] + tiles: [], + extends: {} }]; } [this.parent.currentTileLayer] = this.opts.room.tiles; @@ -94,7 +95,8 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix if (e.inputValue && Number(e.inputValue)) { var layer = { depth: Number(e.inputValue), - tiles: [] + tiles: [], + extends: {} }; this.opts.room.tiles.push(layer); this.parent.currentTileLayer = layer; @@ -110,6 +112,9 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix }; this.changeTileLayer = e => { this.parent.currentTileLayer = this.opts.room.tiles[Number(e.target.value)]; + if (!this.parent.currentTileLayer.extends) { + this.parent.currentTileLayer.extends = {}; + } this.parent.currentTileLayerId = Number(e.target.value); }; this.switchTiledImage = () => { From d138931758be4cf7dd7a5e74339ec655203df214 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 12:17:01 +1200 Subject: [PATCH 50/86] :sparkles: Bitmap fonts --- .editorconfig | 18 ++ .gitignore | 2 +- .vscode/launch.json | 16 ++ README.md | 4 +- app/data/ct.release/inputs.js | 2 + app/data/i18n/English.json | 29 ++- app/package-lock.json | 21 +- app/package.json | 1 + gulpfile.js | 20 +- src/js/gulpWatch.js | 8 +- src/js/loadProject.js | 6 + src/js/migration/1.3.2.js | 11 + src/node_requires/exporter/fonts.js | 97 +++++++- src/node_requires/exporter/index.js | 6 +- src/node_requires/i18n.js | 1 + .../monaco-themes/lucasdracula.json | 4 +- .../fonts/bitmapFontGenerator/index.js | 153 ++++++++++++ .../fonts/bitmapFontGenerator/util.js | 113 +++++++++ src/node_requires/resources/projects/index.js | 14 +- .../debugger/debugger-screen-embedded.tag | 231 ++++++++++++++++++ src/riotTags/font-editor.tag | 79 ++++-- src/riotTags/fonts-panel.tag | 7 +- src/riotTags/main-menu.tag | 64 ++++- src/riotTags/shared/asset-viewer.tag | 2 +- src/riotTags/shared/copy-icon.tag | 19 ++ src/styl/common.styl | 7 + src/styl/inputs.styl | 7 +- src/styl/tags/debugger/debugger-screen.styl | 7 +- src/styl/tags/debugger/debugger-toolbar.styl | 2 +- src/styl/themeDay.styl | 7 - src/styl/themeHorizon.styl | 8 - src/styl/themeLucasDracula.styl | 9 - src/styl/themeNight.styl | 7 - src/styl/themeSpringStream.styl | 7 - src/styl/typography.styl | 11 +- 35 files changed, 913 insertions(+), 87 deletions(-) create mode 100644 .editorconfig create mode 100644 .vscode/launch.json create mode 100644 src/node_requires/resources/fonts/bitmapFontGenerator/index.js create mode 100644 src/node_requires/resources/fonts/bitmapFontGenerator/util.js create mode 100644 src/riotTags/debugger/debugger-screen-embedded.tag create mode 100644 src/riotTags/shared/copy-icon.tag diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1fd1a5a7f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# EditorConfig: https://EditorConfig.org + +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,ts,styl,pug}] +indent_style = space +indent_size = 4 +charset = utf-8 + +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 +charset = utf-8 diff --git a/.gitignore b/.gitignore index 7e012c86b..750862889 100644 --- a/.gitignore +++ b/.gitignore @@ -78,7 +78,7 @@ app/export *.ict.recovery # editor-specific -/.vscode +.vscode/settings.json # docs docs/db.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..77c276ac7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "nwjs", + "request": "launch", + "name": "Launch ct.js", + "nwjsVersion": "0.34.5", + "webRoot": "${workspaceFolder}/app", + "reloadAfterAttached": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index a741c5b98..a4cce1568 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ gulp -f devSetup.gulpfile.js gulp ``` +VSCode can use [this extension](https://marketplace.visualstudio.com/items?itemName=ruakr.vsc-nwjs) to run ct.js with an attached debugger. Use `gulp dev` instead of just `gulp` to run a dev service with live-reloading without opening ct.js in its default manner. + ## Releasing ct.js This is left for emergencies only, as Travis should prepare binaries for github and send them to itch.io as well @@ -91,4 +93,4 @@ The first run will be slow as it will download nw.js binaries. Next runs will us ## Naming conventions for commits -Use [Gitmoji-flavored Comigoji](https://comigo.gitlab.io/comigoji/#gitmoji) for naming your commits. \ No newline at end of file +Use [Gitmoji-flavored Comigoji](https://comigo.gitlab.io/comigoji/#gitmoji) for naming your commits. diff --git a/app/data/ct.release/inputs.js b/app/data/ct.release/inputs.js index e5f6980b6..5313b8702 100644 --- a/app/data/ct.release/inputs.js +++ b/app/data/ct.release/inputs.js @@ -71,7 +71,9 @@ class CtAction { if (ind !== -1) { this.methodMultipliers[ind] = multiplier; } else { + // eslint-disable-next-line no-console console.warning(`[ct.inputs] An attempt to change multiplier of a non-existent method "${code}" at event ${this.name}`); + // eslint-disable-next-line no-console console.trace(); } } diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index b3697fc7e..742a7d77c 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -126,15 +126,16 @@ }, "menu": { "ctIDE": "ct.IDE", - "exportDesktop": "Export for desktop", + "exportDesktop": "Export for desktop…", "texture": "Textures", - "launch": "Compile and run", + "launch": "Launch", "launchHotkeys": "(F5; Alt+F5 to run in your default browser)", "license": "License", "min": "Windowed", "modules": "Catmods", "patrons": "Patrons", "recentProjects": "Recent projects", + "restart": "Restart", "rooms": "Rooms", "save": "Save project", "startScreen": "Return to the starting screen", @@ -170,7 +171,9 @@ "disableAcceleration": "Disable graphics acceleration (needs restart)", "disableBuiltInDebugger": "Disable built-in debugger", "visitDiscordForGamedevSupport": "Join Discord server for gamedev support", - "postAnIssue": "Post an issue on Github" + "postAnIssue": "Post an issue on Github…", + "openProject": "Open a project…", + "openExample": "Open an example project…" }, "onboarding": { "hoorayHeader": "Wow! You've just created a project!", @@ -349,7 +352,23 @@ "typefacename": "Typeface name:", "fontweight": "Font weight:", "italic": "Is italic?", - "reimport": "Reimport" + "reimport": "Reimport", + "generateBitmapFont": "Also generate a bitmap font", + "bitmapFont": "Bitmap font", + "bitmapFontSize": "Font size:", + "bitmapFontLineHeight": "Line height:", + "resultingBitmapFontName": "Resource name", + "charset": "Charset:", + "charsets": { + "punctuation": "Digits and punctuation (you usually do need this)", + "basicLatin": "Basic Latin", + "latinExtended": "Latin extended", + "cyrillic": "Cyrillic", + "greekCoptic": "Greek and Coptic", + "custom": "Custom", + "allInFont": "Draw everything the font supports" + }, + "customCharsetHint": "Type all the letters you want to include, both in upper and lower case." }, "particleEmitters": { "emittersHeading": "Particle emitters", @@ -543,4 +562,4 @@ "is elegant and beautiful 🎩" ] } -} \ No newline at end of file +} diff --git a/app/package-lock.json b/app/package-lock.json index 1e99c58b4..d47c824ec 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2863,6 +2863,15 @@ "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.0.5.tgz", "integrity": "sha1-Nu/zIgE3nv3xGA+0ReUajiQl+fY=" }, + "opentype.js": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.3.tgz", + "integrity": "sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==", + "requires": { + "string.prototype.codepointat": "^0.2.1", + "tiny-inflate": "^1.0.3" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -3263,6 +3272,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3307,6 +3321,11 @@ "has-flag": "^3.0.0" } }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", @@ -3524,7 +3543,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { diff --git a/app/package.json b/app/package.json index 4ec0f7516..7a4443e8f 100644 --- a/app/package.json +++ b/app/package.json @@ -67,6 +67,7 @@ "monaco-editor": "^0.20.0", "monaco-themes": "^0.3.3", "node-static": "^0.7.11", + "opentype.js": "^1.3.3", "pixi-particles": "^4.2.1", "pixi.js-legacy": "5.1.2", "png2icons": "^2.0.1", diff --git a/gulpfile.js b/gulpfile.js index bda975fc3..60b7aa59e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -23,7 +23,7 @@ const path = require('path'), spawnise = require('./node_requires/spawnise'); -const nwVersion = '0.34.5', +const nwVersion = '0.45.6', platforms = ['osx64', 'win32', 'win64', 'linux32', 'linux64'], nwFiles = ['./app/**', '!./app/export/**', '!./app/projects/**', '!./app/exportDesktop/**', '!./app/cache/**', '!./app/.vscode/**', '!./app/JamGames/**']; @@ -118,7 +118,9 @@ const concatScripts = () => gulp.src(['./src/js/**', '!./src/js/3rdparty/riot.min.js']), gulp.src('./temp/riot.js') ) - .pipe(sourcemaps.init()) + .pipe(sourcemaps.init({ + largeFile: true + })) .pipe(concat('bundle.js')) .pipe(sourcemaps.write()) .pipe(gulp.dest('./app/data/')) @@ -135,12 +137,18 @@ const concatScripts = () => .on('change', fileChangeNotifier); const copyRequires = () => gulp.src('./src/node_requires/**/*') + .pipe(sourcemaps.init()) + // ¯\_(ツ)_/¯ + .pipe(sourcemaps.mapSources((sourcePath) => '../../src/' + sourcePath)) + .pipe(sourcemaps.write()) .pipe(gulp.dest('./app/data/node_requires')); const compileScripts = gulp.series(compileRiot, concatScripts); const icons = () => - gulp.src('./src/icons/**/*.svg') + gulp.src('./src/icons/**/*.svg', { + base: './src/icons' + }) .pipe(sprite()) .pipe(gulp.dest('./app/data')); @@ -528,7 +536,12 @@ const launchDevMode = done => { launchApp(); done(); }; +const launchDevModeNoNW = done => { + watch(); + done(); +}; const defaultTask = gulp.series(build, launchDevMode); +const devNoNW = gulp.series(build, launchDevModeNoNW); exports.lintJS = lintJS; exports.lintTags = lintTags; @@ -542,5 +555,6 @@ exports.build = build; exports.deploy = deploy; exports.deployOnly = deployOnly; exports.default = defaultTask; +exports.dev = devNoNW; exports.bakeCompletions = bakeCompletions; exports.bakeTypedefs = bakeTypedefs; diff --git a/src/js/gulpWatch.js b/src/js/gulpWatch.js index 066183416..bf43a852a 100644 --- a/src/js/gulpWatch.js +++ b/src/js/gulpWatch.js @@ -5,7 +5,13 @@ const reload = () => { if (!reloading) { reloading = true; - nw.App.quit(); + if (nw.App.fullArgv.find(arg => arg.indexOf('--remote-debugging-port') !== -1)) { + // Seems that we have an external debugger attached. Reload. + nw.Window.get().reload(); + } else { + // Quit and let gulp handle the reload. + nw.App.quit(); + } } }; gulp.watch(['./data/theme*.css', './index.html', './data/bundle.js', './data/js/**.js', './data/node_requires/**/*.js'], reload); diff --git a/src/js/loadProject.js b/src/js/loadProject.js index 03c79f2bf..ea52921f8 100644 --- a/src/js/loadProject.js +++ b/src/js/loadProject.js @@ -176,6 +176,12 @@ }; window.loadProject = proj => { + if (!proj) { + const baseMessage = 'An attempt to open a project with an empty path.'; + alertify.error(baseMessage + ' See the console for the call stack.'); + const err = new Error(baseMessage); + throw err; + } sessionStorage.projname = path.basename(proj); global.projdir = path.dirname(proj) + path.sep + path.basename(proj, '.ict'); diff --git a/src/js/migration/1.3.2.js b/src/js/migration/1.3.2.js index 87ce9e17f..4c2952f24 100644 --- a/src/js/migration/1.3.2.js +++ b/src/js/migration/1.3.2.js @@ -48,6 +48,17 @@ window.migrationProcess.push({ } } + /** + * Fonts can now be exported as bitmap fonts + */ + for (const font of project.fonts) { + font.bitmapFont = font.bitmapFont || false; + font.bitmapFontSize = font.bitmapFontSize || 16; + font.bitmapFontLineHeight = font.bitmapFontLineHeight || 18; + font.charsets = font.charsets || ['allInFont']; + font.customCharset = font.customCharset || ''; + } + resolve(); }) }); diff --git a/src/node_requires/exporter/fonts.js b/src/node_requires/exporter/fonts.js index a6e491256..8cf83facf 100644 --- a/src/node_requires/exporter/fonts.js +++ b/src/node_requires/exporter/fonts.js @@ -31,6 +31,7 @@ const bundleFonts = async function (proj, projdir, writeDir) { css += stringifyFont(font); })); } + await Promise.all(writePromises); return { css, @@ -38,7 +39,101 @@ const bundleFonts = async function (proj, projdir, writeDir) { }; }; +const charSets = { + punctuation: ' !"#$%&\'()*+,-./0123456789:;<=>?@[\\]^_`{|}~', + basicLatin: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', + latinExtended: 'ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏ', + cyrillic: '«»ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ҂о҃о҄о҅о҆о҇о҈о҉ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԦԧԨԩԪԫԬԭԮԯ', + greekCoptic: 'ͰͱͲͳʹ͵Ͷͷͺͻͼͽ;΄΅Ά·ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ϶ϷϸϹϺϻϼϽϾϿ', + custom: '' +}; + +const specialCharMap = { + 38: '&', + 60: '<', + 32: 'space', + 62: '>', + 34: '"' +}; +const charCodeToXMLChar = code => { + if (code in specialCharMap) { + return specialCharMap[code]; + } + return String.fromCharCode(code); +}; + +const generateXML = function generateXML(fontData, ctFont, typefaceName) { + let XMLTemplate = ` + + + + + + `; + + for (const key in fontData.map) { + const c = fontData.map[key]; + XMLTemplate += `\n `; + } + + XMLTemplate += ` + + +`; + + return XMLTemplate; +}; + +const bakeBitmapFonts = function bakeBitmapFonts(proj, projdir, writeDir) { + const generator = require('./../resources/fonts/bitmapFontGenerator'); + const path = require('path'); + return Promise.all(proj.fonts.filter(font => font.bitmapFont) + .map(async font => { + const fCharsets = font.charsets || ['basicLatin']; + let letterList; + if (fCharsets.length === '1' && fCharsets[0] === 'allInFont') { + letterList = false; + } else { + letterList = fCharsets.reduce((acc, charset) => acc + (charSets[charset] || ''), ''); + } + if (fCharsets.indexOf('custom') !== -1) { + letterList += font.customCharset; + } + const settings = { + fill: '#ffffff', + // stroke: '#000000', + list: letterList, + height: font.bitmapFontSize, + margin: 2 + }; + const typefaceName = `${font.typefaceName}_${font.weight}${font.italic ? '_Italic' : ''}`, + xmlPath = `${font.uid}.xml`, + pngPath = `${font.uid}.png`; + const fontPath = path.join(projdir, 'fonts', font.origname); + const drawData = await generator(fontPath, path.join(writeDir, `${font.uid}.png`), settings); + + const xml = generateXML(drawData, font, typefaceName); + + await fs.writeFile(path.join(writeDir, `${font.uid}.xml`), xml, 'utf8'); + + return { + xmlPath, + pngPath, + typefaceName + }; + })) + .then(fontsMetadata => { + const loaderScript = fontsMetadata.reduce((acc, fontMeta) => + acc + `\n.add('${fontMeta.typefaceName}', '${fontMeta.xmlPath}')`, 'PIXI.Loader.shared'); + return { + loaderScript + }; + }); +}; + module.exports = { stringifyFont, - bundleFonts + bundleFonts, + bakeBitmapFonts, + generateXML }; diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index c2d5e5a77..4b139f91c 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -11,7 +11,7 @@ const {stringifyRooms, getStartingRoom} = require('./rooms'); const {stringifyStyles} = require('./styles'); const {stringifyTandems} = require('./emitterTandems'); const {stringifyTypes} = require('./types'); -const {bundleFonts} = require('./fonts'); +const {bundleFonts, bakeBitmapFonts} = require('./fonts'); const {bakeFavicons} = require('./icons'); const parseKeys = function (catmod, str, lib) { @@ -213,14 +213,16 @@ const exportCtProject = async (project, projdir) => { /* assets — run in parallel */ const texturesTask = packImages(project, writeDir); const skeletonsTask = packSkeletons(project, projdir, writeDir); + const bitmapFontsTask = bakeBitmapFonts(project, projdir, writeDir); const favicons = bakeFavicons(project, writeDir); const textures = await texturesTask; const skeletons = await skeletonsTask; + const bitmapFonts = await bitmapFontsTask; await favicons; buffer += (await sources['res.js']) .replace('/*@sndtotal@*/', project.sounds.length) - .replace('/*@res@*/', textures.res + '\n' + skeletons.loaderScript) + .replace('/*@res@*/', textures.res + '\n' + skeletons.loaderScript + '\n' + bitmapFonts.loaderScript) .replace('/*@textureregistry@*/', textures.registry) .replace('/*@textureatlases@*/', JSON.stringify(textures.atlases)) .replace('/*@skeletonregistry@*/', skeletons.registry) diff --git a/src/node_requires/i18n.js b/src/node_requires/i18n.js index 73bd187da..be0e70d8f 100644 --- a/src/node_requires/i18n.js +++ b/src/node_requires/i18n.js @@ -6,6 +6,7 @@ var i18n; const loadLanguage = lang => { var voc; + console.log('hello'); try { voc = fs.readJSONSync(`./data/i18n/${lang}.json`); } catch (e) { diff --git a/src/node_requires/monaco-themes/lucasdracula.json b/src/node_requires/monaco-themes/lucasdracula.json index d6f819e73..5ed913ee5 100644 --- a/src/node_requires/monaco-themes/lucasdracula.json +++ b/src/node_requires/monaco-themes/lucasdracula.json @@ -6,7 +6,7 @@ "background": "#11111f", "foreground": "#DAD6DA" }, { - "foreground": "#595d68", + "foreground": "#4f4c58", "fontStyle": "italic", "token": "comment" }, @@ -135,4 +135,4 @@ "textLink.foreground": "#f07178", "editorLink.activeForeground": "#f07178" } -} \ No newline at end of file +} diff --git a/src/node_requires/resources/fonts/bitmapFontGenerator/index.js b/src/node_requires/resources/fonts/bitmapFontGenerator/index.js new file mode 100644 index 000000000..8a4ff4a67 --- /dev/null +++ b/src/node_requires/resources/fonts/bitmapFontGenerator/index.js @@ -0,0 +1,153 @@ +/* eslint-disable max-len */ +const util = require('./util'); +const opentype = require('opentype.js'); + +const draw = function draw(ctx, glyphList, descend, options) { + var dict = {}; + + var drawX = 0; + var drawY = 0; + var drawHeight = options.baseline + descend; + var mg; + + glyphList.forEach((g, index) => { + if (g.glyph === void 0) { + if (options.width !== void 0) { + g.width = options.width; + } + ctx.drawImage(options.missingGlyph, drawX, drawY, g.width, drawHeight); + mg = { + x: drawX, y: drawY, width: g.width, height: drawHeight + }; + } else { + var drawWidth = options.width; + if (drawWidth === void 0) { + drawWidth = g.width; + } + if (drawX + drawWidth > ctx.canvas.width) { + drawX = 0; + drawY += drawHeight + options.margin; + } + var path = g.glyph.getPath(drawX + (drawWidth / 2) - (g.width / 2), drawY + options.baseline, options.height); + path.fill = options.fill; + path.stroke = options.stroke; + path.draw(ctx); + if (index === glyphList.length - 1) { + mg = { + x: drawX, y: drawY, width: drawWidth, height: drawHeight + }; + } else { + g.glyph.unicodes.forEach((unicode) => { + dict[unicode] = { + x: drawX, y: drawY, width: drawWidth, height: drawHeight + }; + }); + } + drawX += drawWidth + options.margin; + } + }); + + return { + map: dict, missingGlyph: mg + }; +}; + +const generateBitmapFont = async function generateBitmapFont(fontSrc, outputPath, options, callback) { + const fs = require('fs-extra'); + const buffer = await fs.readFile(fontSrc); + const font = opentype.parse(buffer.buffer); + + if (!options.list || options.list.length === 0) { + options.list = Object.keys(font.glyphs.glyphs) + .map(code => String.fromCharCode(font.glyphs.glyphs[code].unicode) || ' ') + .join(''); + } + + + var lostChars = []; + var glyphList = []; + Array.from(options.list).forEach((char) => { + const [glyph] = font.stringToGlyphs(char); + glyph.font = font; + if (glyph.unicodes.length === 0) { + lostChars.push(char); + } + const scale = 1 / font.unitsPerEm * options.height; + glyphList.push({ + glyph, + width: Math.ceil(glyph.advanceWidth * scale) + }); + }); + + if (isNaN(options.baseline)) { + options.baseline = util.getMaxBaseline(glyphList, options.height); + } + + // Update baseline value while adding missingGlyph to glyphList + if (options.missingGlyph === void 0 || typeof options.missingGlyph === 'string') { + // eslint-disable-next-line prefer-destructuring + var g = font.glyphs.glyphs[0]; + if (options.missingGlyph) { + g = font.charToGlyph(options.missingGlyph); + } + var scale = 1 / font.unitsPerEm * options.height; + g.font = font; + glyphList.push({ + glyph: g, + width: Math.ceil(g.advanceWidth * scale) + }); + if (options.baseline < (g.yMax || 0) * scale) { + options.baseline = Math.ceil((g.yMax || 0) * scale); + } + } + + var descend = util.getMinDescend(glyphList, options.height); + var adjustedHeight = util.getAdjustedHeight(descend, options.height, options.baseline); + + // Calculate the required canvas size + var canvasSize; + if (options.width === void 0) { + canvasSize = util.calculateCanvasSizeProp(options.list, glyphList, adjustedHeight, options.baseline + descend); + } else { + canvasSize = util.calculateCanvasSize(options.list, options.width, adjustedHeight); + } + + // Check if the created canvas size is valid + if (canvasSize.width > 8192 || canvasSize.height > 8192) { + callback('list is too long'); + return false; + } + if (canvasSize.width === -1 || canvasSize.height === -1) { + callback('char size is too small'); + return false; + } + + var canvas = document.createElement('canvas'); + canvas.width = canvasSize.width; + canvas.height = canvasSize.height; + var ctx = canvas.getContext('2d'); + + /* if (options.noAntiAlias) + ctx.antialias = "none";*/ + + // drawing + var drawResult = draw(ctx, glyphList, descend, options); + + // Notify about characters that could not be drawn + if (lostChars.length > 0) { + // eslint-disable-next-line no-console + console.warn('Cannot find ' + lostChars.join(',') + ' from the given font. ' + + 'Generated image does not include these characters. ' + + 'Try Using other font or characters.'); + } + await util.outputBitmapFont(outputPath, canvas, callback); + return { + map: drawResult.map, + missingGlyph: drawResult.missingGlyph, + width: options.width, + height: adjustedHeight, + canvas + }; +}; + +module.exports = generateBitmapFont; diff --git a/src/node_requires/resources/fonts/bitmapFontGenerator/util.js b/src/node_requires/resources/fonts/bitmapFontGenerator/util.js new file mode 100644 index 000000000..ebbd03c92 --- /dev/null +++ b/src/node_requires/resources/fonts/bitmapFontGenerator/util.js @@ -0,0 +1,113 @@ +/* eslint-disable max-len */ +const fs = require('fs').promises; + +const calculateCanvasSize = function calculateCanvasSize(text, charWidth, charHeight) { + if (charWidth <= 0 || charHeight <= 0) { + return { + width: -1, height: -1 + }; + } + + var textSize = text.split('').length + 1; // +1 is for the missing glyph + var canvasSquareSideSize = 1; + + // Find the length of the side of a square that can contain characters + while ((canvasSquareSideSize / charWidth) * (canvasSquareSideSize / charHeight) < textSize) { + canvasSquareSideSize *= 2; + } + var canvasWidth = canvasSquareSideSize; + + // CanvasSquareSideSize cannot be used because it may not be square + var tmpCanvasHeight = Math.ceil(textSize / Math.floor(canvasWidth / charWidth)) * charHeight; + var canvasHeight = 1; + while (canvasHeight < tmpCanvasHeight) { + canvasHeight *= 2; + } + + return { + width: canvasWidth, height: canvasHeight + }; +}; + +const canGoIn = function canGoIn(canvasSize, glyphList, charHeight) { + var drawX = 0; + var drawY = 0; + + glyphList.forEach(glyph => { + if (drawX + glyph.width > canvasSize.width) { + drawX = 0; + drawY += charHeight; + } + drawX += glyph.width; + }); + + return drawY + charHeight < canvasSize.height; +}; + +const calculateCanvasSizeProp = function calculateCanvasSizeProp( + text, + glyphList, + height, + charHeight +) { + var widthAverage = 0; + var widthMax = 0; + glyphList.forEach(glyph => { + if (glyph.width > widthMax) { + widthMax = glyph.width; + } + widthAverage += glyph.width; + }); + widthAverage /= glyphList.length; + + if (height <= 0) { + return { + width: -1, height: -1 + }; + } + // Use the average value to calculate the approximate size + var canvasSize = calculateCanvasSize(text, widthAverage, height); + // Increase the vertical width until the text can be entered + while (!canGoIn(canvasSize, glyphList, charHeight)) { + canvasSize.height *= 2; + } + return canvasSize; +}; + +const outputBitmapFont = function outputBitmapFont(outputPath, canvas) { + const buffer = Buffer.from(canvas.toDataURL().replace(/^data:image\/\w+;base64,/, ''), 'base64'); + return fs.writeFile(outputPath, buffer); +}; + +const getMaxBaseline = function getMaxBaseline(glyphList, height) { + const baseline = Math.ceil(Math.max(-Infinity, ...glyphList.map(glyph => { + var scale = 1 / glyph.glyph.font.unitsPerEm * height; + return (glyph.glyph.yMax || 0) * scale; + }))); + return baseline; +}; + +const getMinDescend = function getMinDescend(glyphList, height) { + var descend = Math.min(Infinity, ...glyphList.map((g) => { + var scale = 1 / g.glyph.font.unitsPerEm * height; + return (g.glyph.yMin || 0) * scale; + })); + return Math.ceil(Math.abs(descend)); +}; + +const getAdjustedHeight = function getAdjustedHeight(descend, height, baseline) { + var extraDescend = Math.ceil(descend - (height - baseline)); + var adjustedHeight = height; + if (extraDescend > 0) { + adjustedHeight += extraDescend; + } + return adjustedHeight; +}; + +module.exports = { + calculateCanvasSizeProp, + getAdjustedHeight, + outputBitmapFont, + getMaxBaseline, + getMinDescend +}; diff --git a/src/node_requires/resources/projects/index.js b/src/node_requires/resources/projects/index.js index 80cb06232..aa0aaee20 100644 --- a/src/node_requires/resources/projects/index.js +++ b/src/node_requires/resources/projects/index.js @@ -5,7 +5,19 @@ const getDefaultProjectDir = function () { return path.join(nw.App.startPath, 'projects'); }; +const getExamplesDir = function () { + const path = require('path'); + try { + require('gulp'); + // Most likely, we are in a dev environment + return path.join(nw.App.startPath, 'src/examples'); + } catch (e) { + return path.join(nw.App.startPath, 'examples'); + } +}; + module.exports = { defaultProject, - getDefaultProjectDir + getDefaultProjectDir, + getExamplesDir }; diff --git a/src/riotTags/debugger/debugger-screen-embedded.tag b/src/riotTags/debugger/debugger-screen-embedded.tag new file mode 100644 index 000000000..7a91b7a3c --- /dev/null +++ b/src/riotTags/debugger/debugger-screen-embedded.tag @@ -0,0 +1,231 @@ +// + Exposes this.reloadGame +debugger-screen-embedded(class="{opts.class} {flexrow: verticalLayout, flexcol: !verticalLayout}") + webview.tall#thePreview( + partition="persist:trusted" + ref="gameView" allownw + ) + .aResizer(ref="gutter" onmousedown="{gutterMouseDown}" class="{vertical: verticalLayout, horizontal: !verticalLayout}") + .flexfix( + style="flex: 0 0 auto; {verticalLayout? 'width:'+width+'px' : 'height:'+height+'px'}" + ) + webview.tall.flexfix-body( + partition="persist:trusted" src="empty.html" + ref="devtoolsView" allownw nwfaketop + style="overflow: hidden;" + ) + .flexfix-footer.aDebuggerToolbar.noshrink( + class="{vertical: verticalLayout} {tight: (verticalLayout && width < 1000) || (!verticalLayout && window.innerWidth < 1000)}" + ) + .debugger-toolbar-aButton(onclick="{togglePause}" title="{gamePaused? voc.resume : voc.pause}") + svg.feather + use(xlink:href="data/icons.svg#{gamePaused? 'play' : 'pause'}") + span {gamePaused? voc.resume : voc.pause} + .debugger-toolbar-aButton(onclick="{restartGame}" title="{voc.restartGame}") + svg.feather + use(xlink:href="data/icons.svg#rotate-cw") + span {voc.restartGame} + .debugger-toolbar-aButton(onclick="{restartRoom}" title="{voc.restartRoom}") + svg.feather + use(xlink:href="data/icons.svg#room-reload") + span {voc.restartRoom} + .debugger-toolbar-aButton(onclick="{displayRoomSelector}" title="{voc.switchRoom}") + svg.feather + use(xlink:href="data/icons.svg#room-switch") + span {voc.switchRoom} + + .debugger-toolbar-aDivider + + .debugger-toolbar-aButton(onclick="{makeScreenshot}" title="{voc.screenshot}") + svg.feather + use(xlink:href="data/icons.svg#camera") + //.debugger-toolbar-aButton(onclick="{toggleFullscreen}" title="{gameFullscreen? voc.exitFullscreen : voc.enterFullscreen}") + // svg.feather + // use(xlink:href="data/icons.svg#{gameFullscreen? 'minimize' : 'maximize'}-2") + .debugger-toolbar-aButton(onclick="{openQrCodes}" title="{voc.links}") + svg.feather + use(xlink:href="data/icons.svg#smartphone") + .debugger-toolbar-aButton(onclick="{openExternal}" title="{voc.openExternal}") + svg.feather + use(xlink:href="data/icons.svg#external-link") + + .debugger-toolbar-aDivider + + .debugger-toolbar-aButton(onclick="{flipLayout}") + svg.feather + use(xlink:href="data/icons.svg#layout-{verticalLayout? 'horizontal' : 'vertical'}") + debugger-modal(if="{showNetworkingModal}") + script. + this.namespace = 'debuggerToolbar'; + this.mixin(window.riotVoc); + + this.showNetworkingModal = false; + + const passedParams = this.opts.params; + if (passedParams.title) { + document.title = passedParams.title + ' — ct.js'; + } + + /* Gutter logic */ + const minSizeW = 400; + const minSizeH = 200; // This includes the height of all buttons + const getMaxSizeW = () => window.innerWidth - 300; + const getMaxSizeH = () => window.innerHeight - 300; + + this.verticalLayout = localStorage.debuggerLayour !== 'horizontal'; + this.width = Math.max(minSizeW, Math.min(getMaxSizeW(), localStorage.debuggerWidth || 500)); + this.height = Math.max(minSizeH, Math.min(getMaxSizeH(), localStorage.debuggerHeight || 300)); + + // iframes and webviews capture mousemove events needed for resize gutter; + // this overlay will prevent it + const catcher = document.createElement('div'); + const s = catcher.style; + s.position = 'fixed'; + s.left = s.right = s.top = s.bottom = '0'; + s.zIndex = 100; + s.cursor = 'ew-resize'; + + this.gutterMouseDown = () => { + this.dragging = true; + s.cursor = this.verticalLayout ? 'ew-resize' : 'ns-resize'; + document.body.appendChild(catcher); + }; + document.addEventListener('mousemove', e => { + if (!this.dragging) { + return; + } + if (this.verticalLayout) { + this.width = Math.max(minSizeW, Math.min(getMaxSizeW(), window.innerWidth - e.clientX)); + localStorage.debuggerWidth = this.width; + } else { + this.height = Math.max(minSizeH, Math.min(getMaxSizeH(), window.innerHeight - e.clientY)); + localStorage.debuggerHeight = this.height; + } + this.update(); + }); + document.addEventListener('mouseup', () => { + if (this.dragging) { + this.dragging = false; + document.body.removeChild(catcher); + } + }); + this.flipLayout = () => { + this.verticalLayout = !this.verticalLayout; + }; + + /* Bootstrap preview and debug views */ + this.on('mount', () => { + this.refs.gameView.addEventListener('contentload', () => { + this.refs.gameView.showDevTools(true, this.refs.devtoolsView); + setTimeout(() => { + this.refs.devtoolsView.executeScript({ + code: 'DevToolsAPI.showPanel(\'console\')', + mainWorld: true + }); + }, 1000); + this.refs.gameView.focus(); + }); + this.refs.gameView.setAttribute('src', passedParams.link); + }); + + /* Helper methods for buttons */ + this.switchRoom = room => { + this.refs.gameView.executeScript({ + code: `ct.rooms.switch('${room}')`, + mainWorld: true + }); + }; + this.displayRoomSelector = e => { + const menu = new nw.Menu(); + // Query for in-game rooms + this.refs.gameView.executeScript({ + code: 'JSON.stringify(Object.keys(ct.rooms.templates));', + mainWorld: true + }, rooms => { + JSON.parse(rooms).map(room => ({ + label: room, + click: () => { + this.switchRoom(room); + } + })) + .forEach(entry => menu.append(new nw.MenuItem(entry))); + menu.popup(e.clientX, e.clientY); + }); + }; + + /* Buttons' event listeners */ + this.togglePause = () => { + this.refs.gameView.executeScript({ + code: ` + if (PIXI.Ticker.shared.started) { + PIXI.Ticker.shared.stop(); + } else { + PIXI.Ticker.shared.start(); + } + !PIXI.Ticker.shared.started; + `, + mainWorld: true + }, paused => { + if (paused === 'false' || paused === false) { + this.gamePaused = false; + } else { + this.gamePaused = true; + } + this.update(); + }); + }; + this.restartGame = () => { + this.refs.gameView.reload(); + }; + this.restartRoom = () => { + this.refs.gameView.executeScript({ + code: 'ct.rooms.switch(ct.room.name);', + mainWorld: true + }); + }; + this.makeScreenshot = () => { + this.refs.gameView.executeScript({ + code: ` + var renderTexture = PIXI.RenderTexture.create({ + width: ct.pixiApp.renderer.width, + height: ct.pixiApp.renderer.height + }); + ct.pixiApp.renderer.render(ct.pixiApp.stage, renderTexture); + var canvas = ct.pixiApp.renderer.extract.canvas(renderTexture); + var dataURL = canvas.toDataURL('image/png'); + dataURL; + `, + mainWorld: true + }, dataURL => { + [dataURL] = dataURL; + window.showSaveDialog({ + filter: 'image/png', + defaultName: `${passedParams.title || 'Screenshot'}.png` + }).then(filename => { + const fs = require('fs'); + if (!filename) { + return; + } + const screenshotBase64 = dataURL.replace(/^data:image\/\w+;base64,/, ''); + const buf = Buffer.from(screenshotBase64, 'base64'); + const stream = fs.createWriteStream(filename); + stream.end(buf); + }); + }); + }; + this.toggleFullscreen = () => { + this.gameFullscreen = !this.gameFullscreen; + if (this.gameFullscreen) { + this.previewWindow.enterFullscreen(); + } else { + this.previewWindow.leaveFullscreen(); + } + }; + this.openQrCodes = () => { + this.showNetworkingModal = !this.showNetworkingModal; + }; + this.openExternal = () => { + if (passedParams.link) { + nw.Shell.openExternal(passedParams.link); + } + }; diff --git a/src/riotTags/font-editor.tag b/src/riotTags/font-editor.tag index 4e164c65e..40ab97843 100644 --- a/src/riotTags/font-editor.tag +++ b/src/riotTags/font-editor.tag @@ -1,19 +1,53 @@ font-editor.panel.view .panel.pad.left.tall.flexfix .flexfix-body - label.block - b {voc.typefacename} - br - input.wide(type="text" onchange="{wire('this.fontobj.typefaceName')}" value="{fontobj.typefaceName}") - label.block - b {voc.fontweight} - br - select(value="{fontobj.weight}" onchange="{wire('this.fontobj.weight')}") - each val in [100, 200, 300, 400, 500, 600, 700, 800, 900] - option(value=val)= val - label.checkbox - input(type="checkbox" checked="{fontobj.italic}" onchange="{wire('this.fontobj.italic')}") - b {voc.italic} + fieldset + label.block + b {voc.typefacename} + br + input.wide(type="text" onchange="{wire('this.fontobj.typefaceName')}" value="{fontobj.typefaceName}") + label.block + b {voc.fontweight} + br + select(value="{fontobj.weight}" onchange="{wire('this.fontobj.weight')}") + each val in [100, 200, 300, 400, 500, 600, 700, 800, 900] + option(value=val)= val + label.checkbox + input(type="checkbox" checked="{fontobj.italic}" onchange="{wire('this.fontobj.italic')}") + b {voc.italic} + fieldset + label.checkbox + input(type="checkbox" checked="{fontobj.bitmapFont}" onchange="{wire('this.fontobj.bitmapFont')}") + b {voc.generateBitmapFont} + h3(if="{fontobj.bitmapFont}") {voc.bitmapFont} + fieldset(if="{fontobj.bitmapFont}") + label.block + b {voc.bitmapFontSize} + br + input.wide(value="{fontobj.bitmapFontSize || 16}" onchange="{wire('this.fontobj.bitmapFontSize')}" type="number" min="1" max="144") + label.block + b {voc.bitmapFontLineHeight} + br + input.wide(value="{fontobj.bitmapFontLineHeight || 18}" onchange="{wire('this.fontobj.bitmapFontLineHeight')}" type="number" min="1" max="300") + .block + b {voc.charset} + label.checkbox(each="{val in charsetOptions}") + input( + type="checkbox" value="{val}" + checked="{fontobj.charsets.indexOf(val) !== -1}" + onchange="{toggleCharset}" + ) + | + | {voc.charsets[val]} + label.block(if="{fontobj.charsets.indexOf('custom') !== -1}") + textarea.wide( + value="{fontobj.customCharset}" + onchange="{wire('this.fontobj.customCharset')}" + ) + h3 {voc.resultingBitmapFontName} + copy-icon.toright(text="{fontobj.typefaceName}_{fontobj.weight}{fontobj.italic? '_Italic' : ''}") + code {fontobj.typefaceName}_{fontobj.weight}{fontobj.italic? '_Italic' : ''} + .clear .flexfix-footer button.wide(onclick="{fontSave}") svg.feather @@ -27,6 +61,23 @@ font-editor.panel.view this.mixin(window.riotVoc); this.mixin(window.riotWired); this.fontobj = this.opts.fontobj; + + this.charsetOptions = ['punctuation', 'basicLatin', 'latinExtended', 'cyrillic', 'greekCoptic', 'custom', 'allInFont']; + + this.toggleCharset = e => { + const {charsets} = this.fontobj; + const ind = charsets.indexOf(e.item.val); + if (ind === -1) { + if (e.item.val === 'allInFont' || + charsets.indexOf('allInFont') !== -1) { + charsets.length = 0; + } + charsets.push(e.item.val); + } else { + charsets.splice(ind, 1); + } + }; + this.oldTypefaceName = this.fontobj.typefaceName; this.fontSave = () => { this.parent.editingFont = false; @@ -44,4 +95,4 @@ font-editor.panel.view break; } } - }); \ No newline at end of file + }); diff --git a/src/riotTags/fonts-panel.tag b/src/riotTags/fonts-panel.tag index 0ac90bd03..740ecde5a 100644 --- a/src/riotTags/fonts-panel.tag +++ b/src/riotTags/fonts-panel.tag @@ -153,6 +153,11 @@ fonts-panel.flexfix.tall.fifty italic: false, origname: path.basename(dest), lastmod: Number(new Date()), + pixelFont: false, + pixelFontSize: 16, + pixelFontLineHeight: 18, + charsets: ['allInFont'], + customCharset: '', uid }; global.currentProject.fonts.push(obj); @@ -265,4 +270,4 @@ fonts-panel.flexfix.tall.fifty }); } }; - this.loadFonts(); \ No newline at end of file + this.loadFonts(); diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 674d810f6..bd2300258 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -11,11 +11,13 @@ main-menu.flexcol li.it30(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") svg.feather use(xlink:href="data/icons.svg#save") - li.nbr.it30(onclick="{runProject}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") - svg.feather - use(xlink:href="data/icons.svg#play") ul#mainnav.nav.tabs + li.nbl.it30(onclick="{runProject}" class="{active: tab === 'debug'}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") + svg.feather + use(xlink:href="data/icons.svg#play") + span(if="{tab !== 'debug'}") {voc.launch} + span(if="{tab === 'debug'}") {voc.restart} li(onclick="{changeTab('project')}" class="{active: tab === 'project'}" data-hotkey="Control+1" title="Control+1") svg.feather use(xlink:href="data/icons.svg#sliders") @@ -49,6 +51,7 @@ main-menu.flexcol use(xlink:href="data/icons.svg#room") span {voc.rooms} div.flexitem.relative(if="{global.currentProject}") + debugger-screen-embedded(if="{tab === 'debug'}" params="{debugParams}" data-hotkey-scope="play" ref="debugger") project-settings(show="{tab === 'project'}" data-hotkey-scope="project") modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") @@ -173,9 +176,19 @@ main-menu.flexcol runCtExport(global.currentProject, global.projdir) .then(() => { if (localStorage.disableBuiltInDebugger === 'yes') { + // Open in default browser nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + } else if (this.tab === 'debug') { + // Restart the game as we already have the tab opened + this.refs.debugger.restartGame(); } else { - window.openDebugger(`http://localhost:${server.address().port}`); + // Open the debugger as usual + this.tab = 'debug'; + this.debugParams = { + title: global.currentProject.settings.authoring.title, + link: `http://localhost:${server.address().port}/` + }; + this.update(); } }) .catch(e => { @@ -477,6 +490,46 @@ main-menu.flexcol } }, { type: 'separator' + }, { + label: window.languageJSON.menu.openProject, + icon: 'folder', + click: () => { + alertify.confirm(window.languageJSON.common.reallyexit, () => { + window.showOpenDialog({ + defaultPath: require('./data/node_requires/resources/projects').getDefaultProjectDir(), + title: window.languageJSON.menu.openProject, + filter: '.ict' + }) + .then(projFile => { + if (!projFile) { + return; + } + window.signals.trigger('resetAll'); + window.loadProject(projFile); + }); + }); + } + }, { // The same as "Open project" item, but shows an examples' folder first + label: window.languageJSON.menu.openExample, + click: () => { + alertify.confirm(window.languageJSON.common.reallyexit, () => { + window.showOpenDialog({ + defaultPath: require('./data/node_requires/resources/projects').getExamplesDir(), + title: window.languageJSON.menu.openProject, + filter: '.ict' + }) + .then(projFile => { + if (!projFile) { + return; + } + window.signals.trigger('resetAll'); + window.loadProject(projFile); + }); + }); + } + }, { + label: window.languageJSON.intro.latest, + submenu: recentProjectsSubmenu }, { label: window.languageJSON.menu.startScreen, click: () => { @@ -486,9 +539,6 @@ main-menu.flexcol } }); } - }, { - label: window.languageJSON.intro.latest, - submenu: recentProjectsSubmenu }, { type: 'separator' }, { diff --git a/src/riotTags/shared/asset-viewer.tag b/src/riotTags/shared/asset-viewer.tag index 8c55ceedf..a4651fc0e 100644 --- a/src/riotTags/shared/asset-viewer.tag +++ b/src/riotTags/shared/asset-viewer.tag @@ -128,4 +128,4 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") localStorage[key] = localStorage[key] === 'list' ? 'grid' : 'list'; }; - this.updateList(); \ No newline at end of file + this.updateList(); diff --git a/src/riotTags/shared/copy-icon.tag b/src/riotTags/shared/copy-icon.tag new file mode 100644 index 000000000..f69886418 --- /dev/null +++ b/src/riotTags/shared/copy-icon.tag @@ -0,0 +1,19 @@ +// + Displays a little inline icon that copies a defined string on click. That's it. + + @attribute text (string) + The text to copy + +copy-icon(onclick="{copy}") + svg.feather.a(class="{success: copied}") + use(xlink:href="data/icons.svg#{copied ? 'check' : 'copy'}") + script. + const defaultText = 'There should have been some useful text, but something wrong happened. Please report about it.'; + this.copy = () => { + nw.Clipboard.get().set(this.opts.text || defaultText, 'text'); + this.copied = true; + setTimeout(() => { + this.copied = false; + this.update(); + }, 1000); + }; \ No newline at end of file diff --git a/src/styl/common.styl b/src/styl/common.styl index b9e186b8e..440acf0c4 100644 --- a/src/styl/common.styl +++ b/src/styl/common.styl @@ -30,6 +30,13 @@ body bottom 0 position absolute +.error + color error +.success + color success +.warning + color warning + .borderall border 1px solid borderBright .borderleft diff --git a/src/styl/inputs.styl b/src/styl/inputs.styl index fb5801291..da3753ada 100644 --- a/src/styl/inputs.styl +++ b/src/styl/inputs.styl @@ -116,14 +116,13 @@ input[type="reset"], top 50% transform translate(0, -50%) -/* Большой переключатель */ -siz = 4em +/* Large checkbox/toggle as seen in the modules tab */ .bigpower cursor pointer display block float left - width siz - height siz + width 4em + height @width border-radius 50% 10% 50% 50% background -webkit-linear-gradient(top, background 0, borderPale 100%) box-shadow 0 0 0.2em backgroundDeeper inset diff --git a/src/styl/tags/debugger/debugger-screen.styl b/src/styl/tags/debugger/debugger-screen.styl index d462f69ad..2110d8c03 100644 --- a/src/styl/tags/debugger/debugger-screen.styl +++ b/src/styl/tags/debugger/debugger-screen.styl @@ -13,5 +13,8 @@ height auto &.tight .debugger-toolbar-aButton > span display none -debugger-screen #thePreview - overflow hidden \ No newline at end of file +debugger-screen, debugger-screen-embedded + #thePreview + overflow hidden +debugger-screen-embedded + @extends .tall diff --git a/src/styl/tags/debugger/debugger-toolbar.styl b/src/styl/tags/debugger/debugger-toolbar.styl index 5dfa9a6ba..3f14f723f 100644 --- a/src/styl/tags/debugger/debugger-toolbar.styl +++ b/src/styl/tags/debugger/debugger-toolbar.styl @@ -18,11 +18,11 @@ debugger-toolbar justify-content space-around line-height 38px .&-aDragger, .&-aButton - height 100% align-content stretch padding 0 0.5rem .&-aDragger color borderBright + height 100% cursor move -webkit-app-region drag app-region drag diff --git a/src/styl/themeDay.styl b/src/styl/themeDay.styl index 70f366332..f019ebd2e 100644 --- a/src/styl/themeDay.styl +++ b/src/styl/themeDay.styl @@ -36,13 +36,6 @@ orange = warning theme = 'Day' themeDark = false -.error - color error -.success - color success -.warning - color warning - borderPale = #e1e2e5 borderBright = #c8cdd1 diff --git a/src/styl/themeHorizon.styl b/src/styl/themeHorizon.styl index 628164a90..7bb0b26dd 100644 --- a/src/styl/themeHorizon.styl +++ b/src/styl/themeHorizon.styl @@ -43,14 +43,6 @@ cyan = #59E1E3 theme = 'Horizon' themeDark = true -.error - color error -.success - color success -.warning - color warning - - @require 'hvost.styl' @require '3rdParty/*.styl' diff --git a/src/styl/themeLucasDracula.styl b/src/styl/themeLucasDracula.styl index d64b2ed90..7189a7138 100644 --- a/src/styl/themeLucasDracula.styl +++ b/src/styl/themeLucasDracula.styl @@ -39,15 +39,6 @@ orange = warning theme = 'LucasDracula' themeDark = true -.error - color error -.success - color success -.warning - color warning - - - @require 'hvost.styl' @require '3rdParty/*.styl' diff --git a/src/styl/themeNight.styl b/src/styl/themeNight.styl index 1fd8260c8..2b2404b13 100644 --- a/src/styl/themeNight.styl +++ b/src/styl/themeNight.styl @@ -42,13 +42,6 @@ orange = warning theme = 'Night' themeDark = true -.error - color error -.success - color success -.warning - color warning - @require 'hvost.styl' @require '3rdParty/*.styl' diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index 62eb1bb9a..ec6b603dd 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -63,13 +63,6 @@ orange = warning theme = 'Spring Stream' themeDark = false -.error - color error -.success - color success -.warning - color warning - borderPale = #d6dedd borderBright = #d6dedd diff --git a/src/styl/typography.styl b/src/styl/typography.styl index 655d90b33..15c47c99f 100644 --- a/src/styl/typography.styl +++ b/src/styl/typography.styl @@ -9,9 +9,18 @@ a, .a color act cursor pointer text-decoration none - trans + {trans} &:hover color accent1 + &.error + color error + &.success + color success + &.warning + color warning + .error, .success, .warning + &:hover + opacity 0.8 dd margin 0 dt From 2c63efbb995e2239140b31585e89414761a5452e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 16:42:15 +1200 Subject: [PATCH 51/86] :bug: Remove "hello" log from i18n.js --- src/node_requires/i18n.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node_requires/i18n.js b/src/node_requires/i18n.js index be0e70d8f..73bd187da 100644 --- a/src/node_requires/i18n.js +++ b/src/node_requires/i18n.js @@ -6,7 +6,6 @@ var i18n; const loadLanguage = lang => { var voc; - console.log('hello'); try { voc = fs.readJSONSync(`./data/i18n/${lang}.json`); } catch (e) { From f5a1967e20f6a2288877748cfdb2dd6538ba12ba Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 17:20:59 +1200 Subject: [PATCH 52/86] :bug: Fix linter warnings for ct.fittoscreen --- app/data/ct.libs/fittoscreen/index.js | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/app/data/ct.libs/fittoscreen/index.js b/app/data/ct.libs/fittoscreen/index.js index 22e289b46..758a6ed01 100644 --- a/app/data/ct.libs/fittoscreen/index.js +++ b/app/data/ct.libs/fittoscreen/index.js @@ -1,9 +1,9 @@ -(function (ct) { +(function fittoscreen(ct) { document.body.style.overflow = 'hidden'; var canv = ct.pixiApp.view; - var resize = function() { + var resize = function resize() { const {mode} = ct.fittoscreen; - const pixelScaleModifier = ct.highDensity? (window.devicePixelRatio || 1) : 1; + const pixelScaleModifier = ct.highDensity ? (window.devicePixelRatio || 1) : 1; const kw = window.innerWidth / ct.roomWidth, kh = window.innerHeight / ct.roomHeight; const k = Math.min(kw, kh); @@ -61,15 +61,24 @@ } }; var toggleFullscreen = function () { - var element = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement, + var canvas = document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement || + document.msFullscreenElement, requester = document.getElementById('ct'), - request = requester.requestFullscreen || requester.webkitRequestFullscreen || requester.mozRequestFullScreen || requester.msRequestFullscreen, - exit = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen; - if (!element) { + request = requester.requestFullscreen || + requester.webkitRequestFullscreen || + requester.mozRequestFullScreen || + requester.msRequestFullscreen, + exit = document.exitFullscreen || + document.webkitExitFullscreen || + document.mozCancelFullScreen || + document.msExitFullscreen; + if (!canvas) { var promise = request.call(requester); if (promise) { promise - .catch(function (err) { + .catch(function fullscreenError(err) { console.error('[ct.fittoscreen]', err); }); } @@ -77,13 +86,13 @@ exit.call(document); } }; - var queuedFullscreen = function () { + var queuedFullscreen = function queuedFullscreen() { toggleFullscreen(); document.removeEventListener('mouseup', queuedFullscreen); document.removeEventListener('keyup', queuedFullscreen); document.removeEventListener('click', queuedFullscreen); }; - var queueFullscreen = function() { + var queueFullscreen = function queueFullscreen() { document.addEventListener('mouseup', queuedFullscreen); document.addEventListener('keyup', queuedFullscreen); document.addEventListener('click', queuedFullscreen); @@ -103,7 +112,7 @@ } }); ct.fittoscreen.mode = $mode; - ct.fittoscreen.getIsFullscreen = function () { + ct.fittoscreen.getIsFullscreen = function getIsFullscreen() { return document.fullscreen || document.webkitIsFullScreen || document.mozFullScreen; }; })(ct); From cd244146216c26516303024145af840eefa5ed25 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 17:25:25 +1200 Subject: [PATCH 53/86] :zap: Replace node-static for dev and docs servers with serve-handler --- app/package-lock.json | 107 +++++++++++------- app/package.json | 4 +- src/node_requires/exporter/index.js | 7 +- src/node_requires/platformUtils.js | 19 ++++ .../debugger/debugger-screen-embedded.tag | 7 +- src/riotTags/main-menu.tag | 29 ++--- src/riotTags/notepad-panel.tag | 24 ++-- 7 files changed, 120 insertions(+), 77 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index d47c824ec..9f7603d3e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1686,6 +1686,11 @@ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -1810,11 +1815,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, "commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", @@ -1851,6 +1851,11 @@ "proto-list": "~1.2.1" } }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, "core-js": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", @@ -2193,6 +2198,14 @@ "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz", "integrity": "sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=" }, + "fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "requires": { + "punycode": "^1.3.2" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -2726,10 +2739,18 @@ "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", "integrity": "sha1-izgy7UDIfVH0e7I0kTppinVtGdI=" }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } }, "mimic-response": { "version": "1.0.1", @@ -2788,16 +2809,6 @@ "lower-case": "^1.1.1" } }, - "node-static": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", - "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", - "requires": { - "colors": ">=0.6.0", - "mime": "^1.2.9", - "optimist": ">=0.3.4" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -2872,22 +2883,6 @@ "tiny-inflate": "^1.0.3" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - } - } - }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", @@ -2958,11 +2953,21 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, + "path-to-regexp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", + "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -3091,11 +3096,21 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "optional": true }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -3229,6 +3244,21 @@ "type-fest": "^0.8.0" } }, + "serve-handler": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz", + "integrity": "sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==", + "requires": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "fast-url-parser": "1.1.3", + "mime-types": "2.1.18", + "minimatch": "3.0.4", + "path-is-inside": "1.0.2", + "path-to-regexp": "2.2.1", + "range-parser": "1.2.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -3531,11 +3561,6 @@ } } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3543,7 +3568,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { diff --git a/app/package.json b/app/package.json index 7a4443e8f..b2c25d77c 100644 --- a/app/package.json +++ b/app/package.json @@ -39,7 +39,7 @@ "url": "https://github.com/ct-js/ct-js.git" }, "scripts": { - "start": "electron ." + "start": "gulp" }, "maintainers": [ { @@ -66,11 +66,11 @@ "maxrects-packer": "^2.6.0", "monaco-editor": "^0.20.0", "monaco-themes": "^0.3.3", - "node-static": "^0.7.11", "opentype.js": "^1.3.3", "pixi-particles": "^4.2.1", "pixi.js-legacy": "5.1.2", "png2icons": "^2.0.1", + "serve-handler": "^6.1.3", "ttf2woff": "^2.0.2", "unzipper": "^0.10.11" } diff --git a/src/node_requires/exporter/index.js b/src/node_requires/exporter/index.js index 4b139f91c..222a7f0a2 100644 --- a/src/node_requires/exporter/index.js +++ b/src/node_requires/exporter/index.js @@ -65,17 +65,14 @@ const injectModules = injects => // async } })); -const makeWritableDir = async () => { - const {getWritableDir} = require('./../platformUtils'); - writeDir = path.join(await getWritableDir(), 'export'); -}; // eslint-disable-next-line max-lines-per-function const exportCtProject = async (project, projdir) => { currentProject = project; const {languageJSON} = require('./../i18n'); const {settings} = project; - await makeWritableDir(); + const {getExportDir} = require('./../platformUtils'); + writeDir = await getExportDir(); if (project.rooms.length < 1) { throw new Error(languageJSON.common.norooms); diff --git a/src/node_requires/platformUtils.js b/src/node_requires/platformUtils.js index 9133c8439..c6ccb5444 100644 --- a/src/node_requires/platformUtils.js +++ b/src/node_requires/platformUtils.js @@ -64,4 +64,23 @@ const mod = { }); } }; + +{ + let exportDir, exportDirPromise; + // We compute a directory once and store it forever + mod.getExportDir = () => { + if (exportDir) { + return Promise.resolve(exportDir); + } + if (exportDirPromise) { + return exportDirPromise; + } + exportDirPromise = mod.getWritableDir().then(dir => { + exportDir = require('path').join(dir, 'export'); + return exportDir; + }); + return exportDirPromise; + }; +} + module.exports = mod; diff --git a/src/riotTags/debugger/debugger-screen-embedded.tag b/src/riotTags/debugger/debugger-screen-embedded.tag index 7a91b7a3c..675a3616d 100644 --- a/src/riotTags/debugger/debugger-screen-embedded.tag +++ b/src/riotTags/debugger/debugger-screen-embedded.tag @@ -116,13 +116,14 @@ debugger-screen-embedded(class="{opts.class} {flexrow: verticalLayout, flexcol: /* Bootstrap preview and debug views */ this.on('mount', () => { this.refs.gameView.addEventListener('contentload', () => { - this.refs.gameView.showDevTools(true, this.refs.devtoolsView); - setTimeout(() => { + this.refs.devtoolsView.addEventListener('contentload', () => { this.refs.devtoolsView.executeScript({ code: 'DevToolsAPI.showPanel(\'console\')', mainWorld: true }); - }, 1000); + this.refs.gameView.focus(); + }); + this.refs.gameView.showDevTools(true, this.refs.devtoolsView); this.refs.gameView.focus(); }); this.refs.gameView.setAttribute('src', passedParams.link); diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index bd2300258..bdeee6d2b 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -153,22 +153,23 @@ main-menu.flexcol }); this.saveRecoveryDebounce(); - const {getWritableDir} = require('./data/node_requires/platformUtils'); + const {getExportDir} = require('./data/node_requires/platformUtils'); // Run a local server for ct.js games let fileServer; - getWritableDir().then(dir => { - const nstatic = require('node-static'); - fileServer = new nstatic.Server(path.join(dir, '/export/'), { - cache: false, - serverInfo: 'ctjsgameeditor' + getExportDir().then(dir => { + console.log(dir); + const fileServerSettings = { + public: dir, + cleanUrls: true + }; + const handler = require('serve-handler'); + fileServer = require('http').createServer((request, response) => { + return handler(request, response, fileServerSettings); }); + fileServer.listen(0, () => { + console.info(`[ct.debugger] Running dev server at http://localhost:${fileServer.address().port}`); }); - const server = require('http').createServer((request, response) => { - request.addListener('end', () => { - fileServer.serve(request, response); - }).resume(); }); - server.listen(0); this.runProject = () => { document.body.style.cursor = 'progress'; @@ -177,7 +178,7 @@ main-menu.flexcol .then(() => { if (localStorage.disableBuiltInDebugger === 'yes') { // Open in default browser - nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + nw.Shell.openExternal(`http://localhost:${fileServer.address().port}/`); } else if (this.tab === 'debug') { // Restart the game as we already have the tab opened this.refs.debugger.restartGame(); @@ -186,7 +187,7 @@ main-menu.flexcol this.tab = 'debug'; this.debugParams = { title: global.currentProject.settings.authoring.title, - link: `http://localhost:${server.address().port}/` + link: `http://localhost:${fileServer.address().port}/` }; this.update(); } @@ -203,7 +204,7 @@ main-menu.flexcol const runCtExport = require('./data/node_requires/exporter'); runCtExport(global.currentProject, global.projdir) .then(() => { - nw.Shell.openExternal(`http://localhost:${server.address().port}/`); + nw.Shell.openExternal(`http://localhost:${fileServer.address().port}/`); }); }; hotkey.on('Alt+F5', this.runProjectAlt); diff --git a/src/riotTags/notepad-panel.tag b/src/riotTags/notepad-panel.tag index f89837d19..2ec9cc7c0 100644 --- a/src/riotTags/notepad-panel.tag +++ b/src/riotTags/notepad-panel.tag @@ -68,7 +68,7 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") localStorage.UItheme === 'Night' || localStorage.UItheme === 'Horizon'; this.backToHome = () => { - this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}/`; + this.refs.helpIframe.contentWindow.location = `http://localhost:${fileServer.address().port}/`; }; this.on('update', () => { @@ -100,22 +100,22 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") this.notepadglobal.dispose(); }); - const nstatic = require('node-static'); - const fileServer = new nstatic.Server('data/docs/', { - cache: false, - serverInfo: 'ctjsgameeditor' + const fileServerSettings = { + public: 'data/docs/', + cleanUrls: true + }; + const handler = require('serve-handler'); + fileServer = require('http').createServer((request, response) => { + return handler(request, response, fileServerSettings); }); - - this.server = require('http').createServer(function staticServerHandler(request, response) { - request.addListener('end', function serveFile() { - fileServer.serve(request, response); - }).resume(); + fileServer.listen(0, () => { + console.info(`[ct.docs] Running docs server at http://localhost:${fileServer.address().port}`); }); - this.server.listen(0); + this.server = fileServer; var openDocs = e => { this.changeTab('helppages')(); - this.refs.helpIframe.contentWindow.location = `http://localhost:${this.server.address().port}${e.path || '/'}`; + this.refs.helpIframe.contentWindow.location = `http://localhost:${fileServer.address().port}${e.path || '/'}`; this.opened = true; this.update(); }; From 6fbd0eae3d3bc6d5bbc789e09816c1914cb47c0f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 17:25:57 +1200 Subject: [PATCH 54/86] :zap: Show a loading icon while exporting project --- src/riotTags/main-menu.tag | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index bdeee6d2b..57558557f 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -8,13 +8,15 @@ main-menu.flexcol li.it30(onclick="{changeTab('patrons')}" title="{voc.patrons}" class="{active: tab === 'patrons'}") svg.feather use(xlink:href="data/icons.svg#heart") - li.it30(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") + li.it30.nbr(onclick="{saveProject}" title="{voc.save} (Control+S)" data-hotkey="Control+s") svg.feather use(xlink:href="data/icons.svg#save") ul#mainnav.nav.tabs li.nbl.it30(onclick="{runProject}" class="{active: tab === 'debug'}" title="{voc.launch} {voc.launchHotkeys}" data-hotkey="F5") - svg.feather + svg.feather.rotateccw(show="{exportingProject}") + use(xlink:href="data/icons.svg#refresh-ccw") + svg.feather(hide="{exportingProject}") use(xlink:href="data/icons.svg#play") span(if="{tab !== 'debug'}") {voc.launch} span(if="{tab === 'debug'}") {voc.restart} @@ -168,11 +170,13 @@ main-menu.flexcol }); fileServer.listen(0, () => { console.info(`[ct.debugger] Running dev server at http://localhost:${fileServer.address().port}`); - }); + }); }); this.runProject = () => { document.body.style.cursor = 'progress'; + this.exportingProject = true; + this.update(); const runCtExport = require('./data/node_requires/exporter'); runCtExport(global.currentProject, global.projdir) .then(() => { @@ -189,7 +193,6 @@ main-menu.flexcol title: global.currentProject.settings.authoring.title, link: `http://localhost:${fileServer.address().port}/` }; - this.update(); } }) .catch(e => { @@ -198,6 +201,8 @@ main-menu.flexcol }) .finally(() => { document.body.style.cursor = ''; + this.exportingProject = false; + this.update(); }); }; this.runProjectAlt = () => { From 3b85004ea04b3ac7bccf83faa5f8745b8c6066d3 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 17:40:12 +1200 Subject: [PATCH 55/86] :construction: Tag catmods with categories Current cateory list: - customization - utilities - media - misc - desktop - motionPlanning - inputs - fx - mobile - integrations --- app/data/ct.libs/akatemplate/module.json | 5 ++++- app/data/ct.libs/assert/module.json | 5 ++++- app/data/ct.libs/capture/module.json | 5 ++++- app/data/ct.libs/cutscene/module.json | 5 ++++- app/data/ct.libs/desktop/module.json | 5 ++++- app/data/ct.libs/eqs/module.json | 5 ++++- app/data/ct.libs/fittoscreen/module.json | 6 +++++- app/data/ct.libs/flow/module.json | 5 ++++- app/data/ct.libs/fs/module.json | 5 ++++- app/data/ct.libs/gamepad/module.json | 3 +++ app/data/ct.libs/inherit/module.json | 5 ++++- app/data/ct.libs/keyboard.legacy/module.json | 5 ++++- app/data/ct.libs/keyboard.polyfill/module.json | 5 ++++- app/data/ct.libs/keyboard/module.json | 15 +++++++++------ app/data/ct.libs/mouse.legacy/module.json | 5 ++++- app/data/ct.libs/mouse/module.json | 5 ++++- app/data/ct.libs/place.legacy/module.json | 5 ++++- app/data/ct.libs/place/module.json | 5 ++++- app/data/ct.libs/random/module.json | 5 ++++- app/data/ct.libs/sound.howler/module.json | 5 ++++- app/data/ct.libs/sprite/module.json | 5 ++++- app/data/ct.libs/touch/module.json | 5 ++++- app/data/ct.libs/transition/module.json | 5 ++++- app/data/ct.libs/tween/module.json | 6 +++++- app/data/ct.libs/vkeys/module.json | 6 +++++- app/data/ct.libs/yarn/module.json | 5 ++++- 26 files changed, 111 insertions(+), 30 deletions(-) diff --git a/app/data/ct.libs/akatemplate/module.json b/app/data/ct.libs/akatemplate/module.json index 858a8b782..e168c8f54 100644 --- a/app/data/ct.libs/akatemplate/module.json +++ b/app/data/ct.libs/akatemplate/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "customization" + ] }, "fields": [ { diff --git a/app/data/ct.libs/assert/module.json b/app/data/ct.libs/assert/module.json index 9f088cfc1..ac91b717b 100644 --- a/app/data/ct.libs/assert/module.json +++ b/app/data/ct.libs/assert/module.json @@ -6,6 +6,9 @@ "authors": [{ "name": "Comigo", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "utilities" + ] } } diff --git a/app/data/ct.libs/capture/module.json b/app/data/ct.libs/capture/module.json index 3154f1940..a130e05fd 100644 --- a/app/data/ct.libs/capture/module.json +++ b/app/data/ct.libs/capture/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "media" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/cutscene/module.json b/app/data/ct.libs/cutscene/module.json index dda1a7c7c..113fb2d56 100644 --- a/app/data/ct.libs/cutscene/module.json +++ b/app/data/ct.libs/cutscene/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "media" + ] } } diff --git a/app/data/ct.libs/desktop/module.json b/app/data/ct.libs/desktop/module.json index 8fe3a3006..3b20ef686 100644 --- a/app/data/ct.libs/desktop/module.json +++ b/app/data/ct.libs/desktop/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "desktop" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/eqs/module.json b/app/data/ct.libs/eqs/module.json index 1d7b9adab..964f0bacc 100644 --- a/app/data/ct.libs/eqs/module.json +++ b/app/data/ct.libs/eqs/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "motionPlanning" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/fittoscreen/module.json b/app/data/ct.libs/fittoscreen/module.json index 67cc2b79a..3fbf99b95 100644 --- a/app/data/ct.libs/fittoscreen/module.json +++ b/app/data/ct.libs/fittoscreen/module.json @@ -5,7 +5,11 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "utilities", + "desktop" + ] }, "fields": [{ "name": "Mode", diff --git a/app/data/ct.libs/flow/module.json b/app/data/ct.libs/flow/module.json index a1f53275b..549fe0485 100644 --- a/app/data/ct.libs/flow/module.json +++ b/app/data/ct.libs/flow/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "utilities" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/fs/module.json b/app/data/ct.libs/fs/module.json index 5b2713e85..a841f1108 100644 --- a/app/data/ct.libs/fs/module.json +++ b/app/data/ct.libs/fs/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "desktop" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/gamepad/module.json b/app/data/ct.libs/gamepad/module.json index 6ba1bd97b..0e29e1c43 100644 --- a/app/data/ct.libs/gamepad/module.json +++ b/app/data/ct.libs/gamepad/module.json @@ -15,6 +15,9 @@ "name": "Comigo", "mail": "admin@nersta.ru" } + ], + "categories": [ + "inputs" ] }, "inputMethods": { diff --git a/app/data/ct.libs/inherit/module.json b/app/data/ct.libs/inherit/module.json index ea2151f50..4db2671f8 100644 --- a/app/data/ct.libs/inherit/module.json +++ b/app/data/ct.libs/inherit/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "utilities" + ] }, "typeExtends": [{ "name": "Parent", diff --git a/app/data/ct.libs/keyboard.legacy/module.json b/app/data/ct.libs/keyboard.legacy/module.json index d33ed6b82..e97d51748 100644 --- a/app/data/ct.libs/keyboard.legacy/module.json +++ b/app/data/ct.libs/keyboard.legacy/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs" + ] } } diff --git a/app/data/ct.libs/keyboard.polyfill/module.json b/app/data/ct.libs/keyboard.polyfill/module.json index 87e95d9fc..caff6971d 100644 --- a/app/data/ct.libs/keyboard.polyfill/module.json +++ b/app/data/ct.libs/keyboard.polyfill/module.json @@ -7,6 +7,9 @@ "mail": "admin@nersta.ru" }, { "name": "Joshua Bell" - }] + }], + "categories": [ + "inputs" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/keyboard/module.json b/app/data/ct.libs/keyboard/module.json index a930a140c..9a488a1c1 100644 --- a/app/data/ct.libs/keyboard/module.json +++ b/app/data/ct.libs/keyboard/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs" + ] }, "inputMethods": { "KeyA": "A", @@ -36,10 +39,10 @@ "KeyZ": "Z", "Space": "Space", - "ArrowLeft": "Left Arrow", - "ArrowUp": "Up Arrow", - "ArrowRight": "Right Arrow", - "ArrowDown": "Down Arrow", + "ArrowLeft": "Left Arrow", + "ArrowUp": "Up Arrow", + "ArrowRight": "Right Arrow", + "ArrowDown": "Down Arrow", "Backspace": "Backspace", "Delete": "Delete", @@ -113,7 +116,7 @@ "Numpad Add": "Numpad +", "Numpad Decimal": "Numpad .", "Numpad Enter": "Numpad Enter", - + "NumLock": "NumLock", "Unknown": "Unknown" diff --git a/app/data/ct.libs/mouse.legacy/module.json b/app/data/ct.libs/mouse.legacy/module.json index fca765076..5dd62059e 100644 --- a/app/data/ct.libs/mouse.legacy/module.json +++ b/app/data/ct.libs/mouse.legacy/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs" + ] } } diff --git a/app/data/ct.libs/mouse/module.json b/app/data/ct.libs/mouse/module.json index 482bdfefc..a95337646 100644 --- a/app/data/ct.libs/mouse/module.json +++ b/app/data/ct.libs/mouse/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs" + ] }, "inputMethods": { "Left": "Left mouse button", diff --git a/app/data/ct.libs/place.legacy/module.json b/app/data/ct.libs/place.legacy/module.json index 5a7be51ce..b40c987ad 100644 --- a/app/data/ct.libs/place.legacy/module.json +++ b/app/data/ct.libs/place.legacy/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "motionPlanning" + ] }, "fields": [{ "name": "Grid size X", diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 67c91e7c4..7e50277b6 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "motionPlanning" + ] }, "fields": [{ "name": "Partitioning", diff --git a/app/data/ct.libs/random/module.json b/app/data/ct.libs/random/module.json index 8614a2ed8..306a8011d 100644 --- a/app/data/ct.libs/random/module.json +++ b/app/data/ct.libs/random/module.json @@ -5,6 +5,9 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "utilities" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/sound.howler/module.json b/app/data/ct.libs/sound.howler/module.json index 6890b04ca..066e9aa79 100644 --- a/app/data/ct.libs/sound.howler/module.json +++ b/app/data/ct.libs/sound.howler/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "media" + ] }, "fields": [{ "name": "Default max hearing distance", diff --git a/app/data/ct.libs/sprite/module.json b/app/data/ct.libs/sprite/module.json index 033402c51..732507b4e 100644 --- a/app/data/ct.libs/sprite/module.json +++ b/app/data/ct.libs/sprite/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "media" + ] }, "fields": [{ "name": "Sprite definitions", diff --git a/app/data/ct.libs/touch/module.json b/app/data/ct.libs/touch/module.json index 5499fc3b6..de93dd3ae 100644 --- a/app/data/ct.libs/touch/module.json +++ b/app/data/ct.libs/touch/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs" + ] }, "fields": [{ "name": "Detect mouse events as touch events", diff --git a/app/data/ct.libs/transition/module.json b/app/data/ct.libs/transition/module.json index d87560ae3..0d51306f7 100644 --- a/app/data/ct.libs/transition/module.json +++ b/app/data/ct.libs/transition/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "fx" + ] }, "dependencies": ["tween"] } \ No newline at end of file diff --git a/app/data/ct.libs/tween/module.json b/app/data/ct.libs/tween/module.json index 2dd7e72ea..31ed21452 100644 --- a/app/data/ct.libs/tween/module.json +++ b/app/data/ct.libs/tween/module.json @@ -5,6 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "fx", + "utilities" + ] } } \ No newline at end of file diff --git a/app/data/ct.libs/vkeys/module.json b/app/data/ct.libs/vkeys/module.json index f894aeb7c..5e5a97b59 100644 --- a/app/data/ct.libs/vkeys/module.json +++ b/app/data/ct.libs/vkeys/module.json @@ -5,7 +5,11 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "inputs", + "mobile" + ] }, "inputMethods": { "Vk1": "Virtual key 1", diff --git a/app/data/ct.libs/yarn/module.json b/app/data/ct.libs/yarn/module.json index bb145affe..6f8303453 100644 --- a/app/data/ct.libs/yarn/module.json +++ b/app/data/ct.libs/yarn/module.json @@ -5,7 +5,10 @@ "authors": [{ "name": "Cosmo Myzrail Gorynych", "mail": "admin@nersta.ru" - }] + }], + "categories": [ + "integrations" + ] }, "fields": [{ "name": "Skip empty lines", From baec352578c47100767b302ec368dae32a7a9082 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 3 Aug 2020 23:55:18 +1200 Subject: [PATCH 56/86] :construction: Modules' taglines --- app/data/ct.libs/akatemplate/module.json | 3 +- app/data/ct.libs/assert/module.json | 29 +- app/data/ct.libs/capture/module.json | 3 +- app/data/ct.libs/cutscene/module.json | 1 + app/data/ct.libs/desktop/module.json | 3 +- app/data/ct.libs/eqs/module.json | 3 +- app/data/ct.libs/fittoscreen/module.json | 3 +- app/data/ct.libs/flow/module.json | 3 +- app/data/ct.libs/fs/module.json | 3 +- app/data/ct.libs/gamepad/module.json | 93 +++---- app/data/ct.libs/inherit/module.json | 37 +-- app/data/ct.libs/keyboard.legacy/module.json | 27 +- .../ct.libs/keyboard.polyfill/module.json | 3 +- app/data/ct.libs/keyboard/module.json | 249 +++++++++--------- app/data/ct.libs/mouse.legacy/module.json | 1 + app/data/ct.libs/mouse/module.json | 1 + app/data/ct.libs/place.legacy/module.json | 1 + app/data/ct.libs/place/module.json | 115 ++++---- app/data/ct.libs/random/module.json | 3 +- app/data/ct.libs/sound.howler/module.json | 3 +- app/data/ct.libs/sprite/module.json | 3 +- app/data/ct.libs/touch/module.json | 3 +- app/data/ct.libs/transition/module.json | 3 +- app/data/ct.libs/tween/module.json | 3 +- app/data/ct.libs/vkeys/module.json | 1 + app/data/ct.libs/yarn/module.json | 3 +- app/package-lock.json | 2 +- 27 files changed, 314 insertions(+), 288 deletions(-) diff --git a/app/data/ct.libs/akatemplate/module.json b/app/data/ct.libs/akatemplate/module.json index e168c8f54..051e5d082 100644 --- a/app/data/ct.libs/akatemplate/module.json +++ b/app/data/ct.libs/akatemplate/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Basic Template", + "tagline": "Add custom HTML and CSS to exported games", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -33,4 +34,4 @@ "type": "textfield" } ] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/assert/module.json b/app/data/ct.libs/assert/module.json index ac91b717b..9dfc1873a 100644 --- a/app/data/ct.libs/assert/module.json +++ b/app/data/ct.libs/assert/module.json @@ -1,14 +1,15 @@ -{ - "main": { - "name": "ct.assert", - "version": "1.0.0", - "packageName": "assert", - "authors": [{ - "name": "Comigo", - "mail": "admin@nersta.ru" - }], - "categories": [ - "utilities" - ] - } -} +{ + "main": { + "name": "ct.assert", + "tagline": "Provides a method ct.assert that helps making quick tests in ct.js projects", + "version": "1.0.0", + "packageName": "assert", + "authors": [{ + "name": "Comigo", + "mail": "admin@nersta.ru" + }], + "categories": [ + "utilities" + ] + } +} diff --git a/app/data/ct.libs/capture/module.json b/app/data/ct.libs/capture/module.json index a130e05fd..721bbe69a 100644 --- a/app/data/ct.libs/capture/module.json +++ b/app/data/ct.libs/capture/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Capture!", + "tagline": "Capture your game or its objects and let your users save shots to PNG", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "media" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/cutscene/module.json b/app/data/ct.libs/cutscene/module.json index 113fb2d56..21bff410f 100644 --- a/app/data/ct.libs/cutscene/module.json +++ b/app/data/ct.libs/cutscene/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.cutscene", + "tagline": "Show videos from your game's folder, Youtube or Vimeo", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/desktop/module.json b/app/data/ct.libs/desktop/module.json index 3b20ef686..e3a31bd1c 100644 --- a/app/data/ct.libs/desktop/module.json +++ b/app/data/ct.libs/desktop/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Desktop features", + "tagline": "Currently provides just ct.desktop.quit() method to close a game on desktop", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "desktop" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/eqs/module.json b/app/data/ct.libs/eqs/module.json index 964f0bacc..f5860f868 100644 --- a/app/data/ct.libs/eqs/module.json +++ b/app/data/ct.libs/eqs/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.eqs", + "tagline": "Environment querying system: apply logic to find the best (or worst) place for positioning", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "motionPlanning" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/fittoscreen/module.json b/app/data/ct.libs/fittoscreen/module.json index 3fbf99b95..454b80984 100644 --- a/app/data/ct.libs/fittoscreen/module.json +++ b/app/data/ct.libs/fittoscreen/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Fit to Screen", + "tagline": "An essencial module that manages fullscreen scaling for you", "version": "4.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -35,4 +36,4 @@ "default": "scaleCover", "type": "radio" }] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/flow/module.json b/app/data/ct.libs/flow/module.json index 549fe0485..451225508 100644 --- a/app/data/ct.libs/flow/module.json +++ b/app/data/ct.libs/flow/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Flow control and timing", + "tagline": "Additional high-level methods to work with asynchronous events, e.g. gate, cumulative delay, retriggerable delay", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "utilities" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/fs/module.json b/app/data/ct.libs/fs/module.json index a841f1108..3924ff7c7 100644 --- a/app/data/ct.libs/fs/module.json +++ b/app/data/ct.libs/fs/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Desktop's file system", + "tagline": "A file system layer that simplifies Node.js API and lets you focus on your data", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "desktop" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/gamepad/module.json b/app/data/ct.libs/gamepad/module.json index 0e29e1c43..ab11a766a 100644 --- a/app/data/ct.libs/gamepad/module.json +++ b/app/data/ct.libs/gamepad/module.json @@ -1,46 +1,47 @@ -{ - "main": { - "name": "Gamepad", - "version": "1.0.0", - "packageName": "gamepad", - "authors": [ - { - "name": "SN" - }, - { - "name": "Araujo921", - "mail": "leedigital@leedigital.com.br" - }, - { - "name": "Comigo", - "mail": "admin@nersta.ru" - } - ], - "categories": [ - "inputs" - ] - }, - "inputMethods": { - "Button1": "Bottom button in right cluster (A / blue cross)", - "Button2": "Right button in right cluster (B / red circle)", - "Button3": "Left button in right cluster (X / pink square)", - "Button4": "Top button in right cluster (Y / green triangle)", - "L1": "Top left front button (Left shoulder / L1)", - "R1": "Top right front button (Right shoulder / R1)", - "L2": "Bottom left front button (Left trigger / L2)", - "R2": "Bottom right front button (Right trigger / R2)", - "Select": "Left button in center cluster (Select)", - "Start": "Right button in center cluster (Start)", - "L3": "Left stick pressed button (L3)", - "R3": "Right stick pressed button (R3)", - "Up": "Top button in left cluster (directional pad)", - "Down": "Bottom button in left cluster (directional pad)", - "Left": "Left button in left cluster (directional pad)", - "Right": "Right button in left cluster (directional pad)", - "Any": "Any known controller's button", - "LStickX": "Horizontal axis for left stick (negative left/positive right)", - "LStickY": "Vertical axis for left stick (negative up/positive down)", - "RStickX": "Horizontal axis for right stick (negative left/positive right)", - "RStickY": "Vertical axis for right stick (negative up/positive down)" - } -} +{ + "main": { + "name": "Gamepad", + "tagline": "Provides all the input methods for gamepads", + "version": "1.0.0", + "packageName": "gamepad", + "authors": [ + { + "name": "SN" + }, + { + "name": "Araujo921", + "mail": "leedigital@leedigital.com.br" + }, + { + "name": "Comigo", + "mail": "admin@nersta.ru" + } + ], + "categories": [ + "inputs" + ] + }, + "inputMethods": { + "Button1": "Bottom button in right cluster (A / blue cross)", + "Button2": "Right button in right cluster (B / red circle)", + "Button3": "Left button in right cluster (X / pink square)", + "Button4": "Top button in right cluster (Y / green triangle)", + "L1": "Top left front button (Left shoulder / L1)", + "R1": "Top right front button (Right shoulder / R1)", + "L2": "Bottom left front button (Left trigger / L2)", + "R2": "Bottom right front button (Right trigger / R2)", + "Select": "Left button in center cluster (Select)", + "Start": "Right button in center cluster (Start)", + "L3": "Left stick pressed button (L3)", + "R3": "Right stick pressed button (R3)", + "Up": "Top button in left cluster (directional pad)", + "Down": "Bottom button in left cluster (directional pad)", + "Left": "Left button in left cluster (directional pad)", + "Right": "Right button in left cluster (directional pad)", + "Any": "Any known controller's button", + "LStickX": "Horizontal axis for left stick (negative left/positive right)", + "LStickY": "Vertical axis for left stick (negative up/positive down)", + "RStickX": "Horizontal axis for right stick (negative left/positive right)", + "RStickY": "Vertical axis for right stick (negative up/positive down)" + } +} diff --git a/app/data/ct.libs/inherit/module.json b/app/data/ct.libs/inherit/module.json index 4db2671f8..14721e0a4 100644 --- a/app/data/ct.libs/inherit/module.json +++ b/app/data/ct.libs/inherit/module.json @@ -1,18 +1,19 @@ -{ - "main": { - "name": "Type inheritance", - "version": "1.0.0", - "authors": [{ - "name": "Cosmo Myzrail Gorynych", - "mail": "admin@nersta.ru" - }], - "categories": [ - "utilities" - ] - }, - "typeExtends": [{ - "name": "Parent", - "type": "type", - "key": "inheritedType@@type" - }] -} +{ + "main": { + "name": "Type inheritance", + "tagline": "Works on top of ct.js v1.4 to bring type inheritance and make your code DRY", + "version": "1.0.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }], + "categories": [ + "utilities" + ] + }, + "typeExtends": [{ + "name": "Parent", + "type": "type", + "key": "inheritedType@@type" + }] +} diff --git a/app/data/ct.libs/keyboard.legacy/module.json b/app/data/ct.libs/keyboard.legacy/module.json index e97d51748..1718acfd5 100644 --- a/app/data/ct.libs/keyboard.legacy/module.json +++ b/app/data/ct.libs/keyboard.legacy/module.json @@ -1,13 +1,14 @@ -{ - "main": { - "name": "Keyboard", - "version": "2.0.0", - "authors": [{ - "name": "Cosmo Myzrail Gorynych", - "mail": "admin@nersta.ru" - }], - "categories": [ - "inputs" - ] - } -} +{ + "main": { + "name": "Keyboard", + "tagline": "The old implementation of ct.keyboard that did not support Actions system", + "version": "2.0.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }], + "categories": [ + "inputs" + ] + } +} diff --git a/app/data/ct.libs/keyboard.polyfill/module.json b/app/data/ct.libs/keyboard.polyfill/module.json index caff6971d..5ba4ae05e 100644 --- a/app/data/ct.libs/keyboard.polyfill/module.json +++ b/app/data/ct.libs/keyboard.polyfill/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Keyboard Polyfill", + "tagline": "A polyfill for ct.keyboard so that it works in older browsers", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -12,4 +13,4 @@ "inputs" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/keyboard/module.json b/app/data/ct.libs/keyboard/module.json index 9a488a1c1..87ecfa8e6 100644 --- a/app/data/ct.libs/keyboard/module.json +++ b/app/data/ct.libs/keyboard/module.json @@ -1,124 +1,125 @@ -{ - "main": { - "name": "Keyboard", - "version": "3.0.0", - "authors": [{ - "name": "Cosmo Myzrail Gorynych", - "mail": "admin@nersta.ru" - }], - "categories": [ - "inputs" - ] - }, - "inputMethods": { - "KeyA": "A", - "KeyB": "B", - "KeyC": "C", - "KeyD": "D", - "KeyE": "E", - "KeyF": "F", - "KeyG": "G", - "KeyH": "H", - "KeyI": "I", - "KeyJ": "J", - "KeyK": "K", - "KeyL": "L", - "KeyM": "M", - "KeyN": "N", - "KeyO": "O", - "KeyP": "P", - "KeyQ": "Q", - "KeyR": "R", - "KeyS": "S", - "KeyT": "T", - "KeyU": "U", - "KeyV": "V", - "KeyW": "W", - "KeyX": "X", - "KeyY": "Y", - "KeyZ": "Z", - - "Space": "Space", - "ArrowLeft": "Left Arrow", - "ArrowUp": "Up Arrow", - "ArrowRight": "Right Arrow", - "ArrowDown": "Down Arrow", - - "Backspace": "Backspace", - "Delete": "Delete", - "End": "End", - "Enter": "Enter", - "Escape": "Escape", - "Home": "Home", - "Insert": "Insert", - "PageUp": "Page Up", - "PageDown": "Page Down", - - "ShiftLeft": "Left Shift", - "ControlLeft": "Left Control", - "AltLeft": "Left Alt", - - "ControlRight": "Right Control", - "AltRight": "Right Alt", - "ShiftRight": "Right Shift", - "ContextMenu": "Context Menu", - - "Backquote": "`", - "Minus": "-", - "Equal": "=", - "BracketLeft": "[", - "BracketRight": "]", - "Semicolon": ":", - "Quote": "\"", - "Comma": ",", - "Period": ".", - "Slash": "/", - "Backslash": "\\", - - "Digit1": "1", - "Digit2": "2", - "Digit3": "3", - "Digit4": "4", - "Digit5": "5", - "Digit6": "6", - "Digit7": "7", - "Digit8": "8", - "Digit9": "9", - "Digit0": "0", - - "F1": "F1", - "F2": "F2", - "F3": "F3", - "F4": "F4", - "F5": "F5", - "F6": "F6", - "F7": "F7", - "F8": "F8", - "F9": "F9", - "F10": "F10", - "F11": "F11", - "F12": "F12", - - "Numpad1": "Numpad 1", - "Numpad2": "Numpad 2", - "Numpad3": "Numpad 3", - "Numpad4": "Numpad 4", - "Numpad5": "Numpad 5", - "Numpad6": "Numpad 6", - "Numpad7": "Numpad 7", - "Numpad8": "Numpad 8", - "Numpad9": "Numpad 9", - "Numpad0": "Numpad 0", - - "Numpad Divide": "Numpad /", - "Numpad Multiply": "Numpad *", - "Numpad Subtract": "Numpad -", - "Numpad Add": "Numpad +", - "Numpad Decimal": "Numpad .", - "Numpad Enter": "Numpad Enter", - - "NumLock": "NumLock", - - "Unknown": "Unknown" - } -} +{ + "main": { + "name": "Keyboard", + "tagline": "Provides input methods for keyboards", + "version": "3.0.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }], + "categories": [ + "inputs" + ] + }, + "inputMethods": { + "KeyA": "A", + "KeyB": "B", + "KeyC": "C", + "KeyD": "D", + "KeyE": "E", + "KeyF": "F", + "KeyG": "G", + "KeyH": "H", + "KeyI": "I", + "KeyJ": "J", + "KeyK": "K", + "KeyL": "L", + "KeyM": "M", + "KeyN": "N", + "KeyO": "O", + "KeyP": "P", + "KeyQ": "Q", + "KeyR": "R", + "KeyS": "S", + "KeyT": "T", + "KeyU": "U", + "KeyV": "V", + "KeyW": "W", + "KeyX": "X", + "KeyY": "Y", + "KeyZ": "Z", + + "Space": "Space", + "ArrowLeft": "Left Arrow", + "ArrowUp": "Up Arrow", + "ArrowRight": "Right Arrow", + "ArrowDown": "Down Arrow", + + "Backspace": "Backspace", + "Delete": "Delete", + "End": "End", + "Enter": "Enter", + "Escape": "Escape", + "Home": "Home", + "Insert": "Insert", + "PageUp": "Page Up", + "PageDown": "Page Down", + + "ShiftLeft": "Left Shift", + "ControlLeft": "Left Control", + "AltLeft": "Left Alt", + + "ControlRight": "Right Control", + "AltRight": "Right Alt", + "ShiftRight": "Right Shift", + "ContextMenu": "Context Menu", + + "Backquote": "`", + "Minus": "-", + "Equal": "=", + "BracketLeft": "[", + "BracketRight": "]", + "Semicolon": ":", + "Quote": "\"", + "Comma": ",", + "Period": ".", + "Slash": "/", + "Backslash": "\\", + + "Digit1": "1", + "Digit2": "2", + "Digit3": "3", + "Digit4": "4", + "Digit5": "5", + "Digit6": "6", + "Digit7": "7", + "Digit8": "8", + "Digit9": "9", + "Digit0": "0", + + "F1": "F1", + "F2": "F2", + "F3": "F3", + "F4": "F4", + "F5": "F5", + "F6": "F6", + "F7": "F7", + "F8": "F8", + "F9": "F9", + "F10": "F10", + "F11": "F11", + "F12": "F12", + + "Numpad1": "Numpad 1", + "Numpad2": "Numpad 2", + "Numpad3": "Numpad 3", + "Numpad4": "Numpad 4", + "Numpad5": "Numpad 5", + "Numpad6": "Numpad 6", + "Numpad7": "Numpad 7", + "Numpad8": "Numpad 8", + "Numpad9": "Numpad 9", + "Numpad0": "Numpad 0", + + "Numpad Divide": "Numpad /", + "Numpad Multiply": "Numpad *", + "Numpad Subtract": "Numpad -", + "Numpad Add": "Numpad +", + "Numpad Decimal": "Numpad .", + "Numpad Enter": "Numpad Enter", + + "NumLock": "NumLock", + + "Unknown": "Unknown" + } +} diff --git a/app/data/ct.libs/mouse.legacy/module.json b/app/data/ct.libs/mouse.legacy/module.json index 5dd62059e..11f2cbb4e 100644 --- a/app/data/ct.libs/mouse.legacy/module.json +++ b/app/data/ct.libs/mouse.legacy/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Mouse Input (legacy)", + "tagline": "A deprecated module to listen to mouse events; does not work with Actions system", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/mouse/module.json b/app/data/ct.libs/mouse/module.json index a95337646..bc8edc474 100644 --- a/app/data/ct.libs/mouse/module.json +++ b/app/data/ct.libs/mouse/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Mouse Input", + "tagline": "Provides input methods for mouse", "version": "3.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/place.legacy/module.json b/app/data/ct.libs/place.legacy/module.json index b40c987ad..f5cf63cda 100644 --- a/app/data/ct.libs/place.legacy/module.json +++ b/app/data/ct.libs/place.legacy/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.place", + "tagline": "Old, unoptimized version of collision library", "version": "2.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 7e50277b6..2dec36c51 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -1,57 +1,58 @@ -{ - "main": { - "name": "ct.place", - "version": "3.1.0", - "authors": [{ - "name": "Cosmo Myzrail Gorynych", - "mail": "admin@nersta.ru" - }], - "categories": [ - "motionPlanning" - ] - }, - "fields": [{ - "name": "Partitioning", - "type": "h2" - }, { - "name": "Grid size X", - "help": "Tells ct.place how to spacially group copies. This should be at least as large as the horizontal side of the biggest colliding sprite of your game.", - "key": "gridX", - "default": 512, - "type": "number" - }, { - "name": "Grid size Y", - "help": "Tells ct.place how to spacially group copies. This should be at least as large as the vertical size of the biggest colliding sprite of your game.", - "key": "gridY", - "default": 512, - "type": "number" - }, { - "name": "Debug mode", - "type": "h2" - }, { - "name": "Enable", - "help": "Displays collision shapes, collision groups and partitions. It will also write additional keys to most colliding objects. Doesn't work on hidden objects.", - "key": "debugMode", - "default": false, - "type": "checkbox" - }, { - "name": "Debug text size", - "key": "debugText", - "default": 16, - "type": "number" - }], - "typeExtends": [{ - "name": "Collision group", - "type": "text", - "collect": true, - "collectScope": "place::ctype", - "key": "ctype" - }], - "tileLayerExtends": [{ - "name": "Collision group", - "type": "text", - "collect": true, - "collectScope": "place::ctype", - "key": "ctype" - }] -} +{ + "main": { + "name": "ct.place", + "tagline": "Collision checks, continuous movement, and basic collision avoidance", + "version": "3.1.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }], + "categories": [ + "motionPlanning" + ] + }, + "fields": [{ + "name": "Partitioning", + "type": "h2" + }, { + "name": "Grid size X", + "help": "Tells ct.place how to spacially group copies. This should be at least as large as the horizontal side of the biggest colliding sprite of your game.", + "key": "gridX", + "default": 512, + "type": "number" + }, { + "name": "Grid size Y", + "help": "Tells ct.place how to spacially group copies. This should be at least as large as the vertical size of the biggest colliding sprite of your game.", + "key": "gridY", + "default": 512, + "type": "number" + }, { + "name": "Debug mode", + "type": "h2" + }, { + "name": "Enable", + "help": "Displays collision shapes, collision groups and partitions. It will also write additional keys to most colliding objects. Doesn't work on hidden objects.", + "key": "debugMode", + "default": false, + "type": "checkbox" + }, { + "name": "Debug text size", + "key": "debugText", + "default": 16, + "type": "number" + }], + "typeExtends": [{ + "name": "Collision group", + "type": "text", + "collect": true, + "collectScope": "place::ctype", + "key": "ctype" + }], + "tileLayerExtends": [{ + "name": "Collision group", + "type": "text", + "collect": true, + "collectScope": "place::ctype", + "key": "ctype" + }] +} diff --git a/app/data/ct.libs/random/module.json b/app/data/ct.libs/random/module.json index 306a8011d..8c12727c4 100644 --- a/app/data/ct.libs/random/module.json +++ b/app/data/ct.libs/random/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.random", + "tagline": "A bunch of functions to generate random stuff", "version": "1.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -10,4 +11,4 @@ "utilities" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/sound.howler/module.json b/app/data/ct.libs/sound.howler/module.json index 066e9aa79..e32d222c2 100644 --- a/app/data/ct.libs/sound.howler/module.json +++ b/app/data/ct.libs/sound.howler/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.sound.howler", + "tagline": "An advanced sound system for 3D sounds and blending", "version": "1.2.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -32,4 +33,4 @@ "default": true, "type": "checkbox" }] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/sprite/module.json b/app/data/ct.libs/sprite/module.json index 732507b4e..30c844ba0 100644 --- a/app/data/ct.libs/sprite/module.json +++ b/app/data/ct.libs/sprite/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Sprite Composer", + "tagline": "Creates new animations from one sprite, freeing you from the necessity to split and stitch frames manually", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -18,4 +19,4 @@ "default": "", "type": "textfield" }] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/touch/module.json b/app/data/ct.libs/touch/module.json index de93dd3ae..bdcc3d479 100644 --- a/app/data/ct.libs/touch/module.json +++ b/app/data/ct.libs/touch/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Touch", + "tagline": "Input methods for touch devices, plus methods to check on-screen button presses", "version": "2.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -34,4 +35,4 @@ "PanY": "Panning by Y" }, "optionalDependencies": ["place"] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/transition/module.json b/app/data/ct.libs/transition/module.json index 0d51306f7..57643a8c9 100644 --- a/app/data/ct.libs/transition/module.json +++ b/app/data/ct.libs/transition/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.transition", + "tagline": "Creates nice and smooth transitions between levels, or when you need them", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -11,4 +12,4 @@ ] }, "dependencies": ["tween"] -} \ No newline at end of file +} diff --git a/app/data/ct.libs/tween/module.json b/app/data/ct.libs/tween/module.json index 31ed21452..8ccfb2caa 100644 --- a/app/data/ct.libs/tween/module.json +++ b/app/data/ct.libs/tween/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.tween", + "tagline": "Methods to smoothly animate values through time", "version": "0.0.1", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -11,4 +12,4 @@ "utilities" ] } -} \ No newline at end of file +} diff --git a/app/data/ct.libs/vkeys/module.json b/app/data/ct.libs/vkeys/module.json index 5e5a97b59..c291f6e6b 100644 --- a/app/data/ct.libs/vkeys/module.json +++ b/app/data/ct.libs/vkeys/module.json @@ -1,6 +1,7 @@ { "main": { "name": "Virtual Keys", + "tagline": "Create on-screen buttons and joysticks and listen to them in Actions system", "version": "2.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/yarn/module.json b/app/data/ct.libs/yarn/module.json index 6f8303453..6286c7b40 100644 --- a/app/data/ct.libs/yarn/module.json +++ b/app/data/ct.libs/yarn/module.json @@ -1,6 +1,7 @@ { "main": { "name": "ct.yarn", + "tagline": "Use YarnSpinner projects to create interactive dialogues in your game", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", @@ -25,4 +26,4 @@ "default": true, "type": "checkbox" }] -} \ No newline at end of file +} diff --git a/app/package-lock.json b/app/package-lock.json index 9f7603d3e..929d2dd98 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -3568,7 +3568,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { From 7f192f7a6ec549fd283be882ac4006f3f78716c8 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 6 Aug 2020 23:42:21 +1200 Subject: [PATCH 57/86] Merge branch 'modules-panel-rework' --- app/data/ct.libs/akatemplate/README.md | 1 - app/data/ct.libs/akatemplate/module.json | 2 +- app/data/ct.libs/assert/README.md | 6 +- app/data/ct.libs/assert/module.json | 2 +- app/data/ct.libs/capture/README.md | 4 +- app/data/ct.libs/capture/module.json | 2 +- app/data/ct.libs/cutscene/README.md | 2 + app/data/ct.libs/cutscene/module.json | 2 +- app/data/ct.libs/desktop/module.json | 2 +- app/data/ct.libs/eqs/module.json | 2 +- app/data/ct.libs/fittoscreen/DOCS.md | 25 - app/data/ct.libs/fittoscreen/README.md | 30 +- app/data/ct.libs/fittoscreen/module.json | 2 +- app/data/ct.libs/flow/module.json | 2 +- app/data/ct.libs/fs/module.json | 2 +- app/data/ct.libs/gamepad/module.json | 2 +- app/data/ct.libs/inherit/module.json | 2 +- app/data/ct.libs/keyboard.legacy/module.json | 2 +- app/data/ct.libs/keyboard.polyfill/README.md | 3 - .../ct.libs/keyboard.polyfill/module.json | 2 +- app/data/ct.libs/keyboard/DOCS.md | 27 - app/data/ct.libs/keyboard/README.md | 32 +- app/data/ct.libs/keyboard/module.json | 2 +- app/data/ct.libs/mouse.legacy/module.json | 2 +- app/data/ct.libs/mouse/DOCS.md | 64 -- app/data/ct.libs/mouse/README.md | 69 +- app/data/ct.libs/mouse/module.json | 2 +- app/data/ct.libs/place.legacy/module.json | 2 +- .../{DOCS.md => docs/Collision checks.md} | 36 +- .../ct.libs/place/docs/Common pitfalls.md | 25 + ...ng custom collision shapes dynamically.md} | 21 +- .../ct.libs/place/docs/Movement methods.md | 22 + app/data/ct.libs/place/docs/Utilities.md | 13 + app/data/ct.libs/place/module.json | 2 +- app/data/ct.libs/random/module.json | 2 +- app/data/ct.libs/sound.howler/module.json | 2 +- app/data/ct.libs/sprite/module.json | 2 +- app/data/ct.libs/touch/module.json | 2 +- app/data/ct.libs/transition/module.json | 2 +- app/data/ct.libs/tween/module.json | 2 +- app/data/ct.libs/vkeys/module.json | 2 +- app/data/ct.libs/yarn/module.json | 2 +- app/data/i18n/Brazilian Portuguese.json | 8 +- app/data/i18n/Chinese Simplified.json | 6 +- app/data/i18n/Dutch.json | 4 - app/data/i18n/English.json | 40 +- app/data/i18n/French.json | 6 +- app/data/i18n/German.json | 6 +- app/data/i18n/Polish.json | 810 +++++++++--------- app/data/i18n/Romanian.json | 6 +- app/data/i18n/Russian.json | 6 +- app/data/i18n/Spanish.json | 6 +- gulpfile.js | 5 + src/icons/film.svg | 1 + src/icons/globe.svg | 1 + src/icons/monitor.svg | 1 + src/icons/refresh-cw.svg | 1 + src/icons/tool.svg | 1 + src/icons/tv.svg | 1 + src/node_requires/resources/modules/index.js | 122 +++ src/riotTags/docs-panel.tag | 76 ++ src/riotTags/main-menu.tag | 14 +- src/riotTags/modules-panel.tag | 347 -------- src/riotTags/notepad-panel.tag | 23 +- .../project-settings/modules/module-meta.tag | 119 +++ .../modules/modules-settings.tag | 196 +++++ .../project-settings/project-settings.tag | 40 +- src/riotTags/shared/collapsible-section.tag | 27 +- src/riotTags/shared/extensions-editor.tag | 3 +- src/styl/buildingBlocks.styl | 2 +- src/styl/tags/docs-panel.styl | 52 ++ src/styl/tags/modules-panel.styl | 52 -- .../tags/settings/modules/module-meta.styl | 33 + .../settings/modules/modules-settings.styl | 40 + src/styl/tags/settings/project-settings.styl | 4 +- src/styl/tags/shared/collapsible-section.styl | 1 + src/styl/tags/shared/extensions-editor.styl | 7 +- src/styl/themeDay.styl | 116 +-- src/styl/themeSpringStream.styl | 246 +++--- src/styl/typography.styl | 115 +-- 80 files changed, 1664 insertions(+), 1312 deletions(-) delete mode 100644 app/data/ct.libs/akatemplate/README.md delete mode 100644 app/data/ct.libs/fittoscreen/DOCS.md delete mode 100644 app/data/ct.libs/keyboard.polyfill/README.md delete mode 100644 app/data/ct.libs/keyboard/DOCS.md delete mode 100644 app/data/ct.libs/mouse/DOCS.md rename app/data/ct.libs/place/{DOCS.md => docs/Collision checks.md} (67%) create mode 100644 app/data/ct.libs/place/docs/Common pitfalls.md rename app/data/ct.libs/place/{README.md => docs/Creating custom collision shapes dynamically.md} (56%) create mode 100644 app/data/ct.libs/place/docs/Movement methods.md create mode 100644 app/data/ct.libs/place/docs/Utilities.md create mode 100644 src/icons/film.svg create mode 100644 src/icons/globe.svg create mode 100644 src/icons/monitor.svg create mode 100644 src/icons/refresh-cw.svg create mode 100644 src/icons/tool.svg create mode 100644 src/icons/tv.svg create mode 100644 src/node_requires/resources/modules/index.js create mode 100644 src/riotTags/docs-panel.tag delete mode 100644 src/riotTags/modules-panel.tag create mode 100644 src/riotTags/project-settings/modules/module-meta.tag create mode 100644 src/riotTags/project-settings/modules/modules-settings.tag create mode 100644 src/styl/tags/docs-panel.styl delete mode 100644 src/styl/tags/modules-panel.styl create mode 100644 src/styl/tags/settings/modules/module-meta.styl create mode 100644 src/styl/tags/settings/modules/modules-settings.styl diff --git a/app/data/ct.libs/akatemplate/README.md b/app/data/ct.libs/akatemplate/README.md deleted file mode 100644 index 8d5dc6b44..000000000 --- a/app/data/ct.libs/akatemplate/README.md +++ /dev/null @@ -1 +0,0 @@ -This module allows you to set custom HTML and CSS to your exported project. \ No newline at end of file diff --git a/app/data/ct.libs/akatemplate/module.json b/app/data/ct.libs/akatemplate/module.json index 051e5d082..4048b9169 100644 --- a/app/data/ct.libs/akatemplate/module.json +++ b/app/data/ct.libs/akatemplate/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Basic Template", - "tagline": "Add custom HTML and CSS to exported games", + "tagline": "Add custom HTML and CSS to exported games.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/assert/README.md b/app/data/ct.libs/assert/README.md index c62fbafad..ca4aa091d 100644 --- a/app/data/ct.libs/assert/README.md +++ b/app/data/ct.libs/assert/README.md @@ -1,10 +1,12 @@ -A tiny module that provides a method `ct.assert(condition, message)` to help making readable tests in ct.js projects. +# assert + +`assert` is a tiny module that provides a method `ct.assert(condition, message)` to help making readable tests in ct.js projects. The `condition` may be either boolean or a function — other values (numbers, strings) will fail. Functions are executed first and then tested against their returned result. There is also a `ct.assert.summary();` call, that shows counted amount of passed and failed tests. -Usage example: +## Usage example ```js ct.assert( diff --git a/app/data/ct.libs/assert/module.json b/app/data/ct.libs/assert/module.json index 9dfc1873a..5eeffe7cb 100644 --- a/app/data/ct.libs/assert/module.json +++ b/app/data/ct.libs/assert/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.assert", - "tagline": "Provides a method ct.assert that helps making quick tests in ct.js projects", + "tagline": "Make quick tests in ct.js projects with ct.assert method.", "version": "1.0.0", "packageName": "assert", "authors": [{ diff --git a/app/data/ct.libs/capture/README.md b/app/data/ct.libs/capture/README.md index 4f037d3a9..8519aa009 100644 --- a/app/data/ct.libs/capture/README.md +++ b/app/data/ct.libs/capture/README.md @@ -1,4 +1,6 @@ -Provides two methods to capture stuff in your game and let users save it as an image. +# Capture! + +This module provides two methods to capture stuff in your game and let users save it as an image. **Note:** this module doesn't work in the ct.js debugger at the moment. diff --git a/app/data/ct.libs/capture/module.json b/app/data/ct.libs/capture/module.json index 721bbe69a..e76b86d83 100644 --- a/app/data/ct.libs/capture/module.json +++ b/app/data/ct.libs/capture/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Capture!", - "tagline": "Capture your game or its objects and let your users save shots to PNG", + "tagline": "Capture your game or its objects and let your users save shots to PNG.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/cutscene/README.md b/app/data/ct.libs/cutscene/README.md index 5d4e45b6c..b5f95c27d 100644 --- a/app/data/ct.libs/cutscene/README.md +++ b/app/data/ct.libs/cutscene/README.md @@ -1,3 +1,5 @@ +# ct.cutscene + A module for showing cutscenes from youtube/vimeo or a direct link to a video. ## `ct.cutscene.show(url)` diff --git a/app/data/ct.libs/cutscene/module.json b/app/data/ct.libs/cutscene/module.json index 21bff410f..cd46ec965 100644 --- a/app/data/ct.libs/cutscene/module.json +++ b/app/data/ct.libs/cutscene/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.cutscene", - "tagline": "Show videos from your game's folder, Youtube or Vimeo", + "tagline": "Show videos from your game's folder, Youtube or Vimeo.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/desktop/module.json b/app/data/ct.libs/desktop/module.json index e3a31bd1c..1d709678b 100644 --- a/app/data/ct.libs/desktop/module.json +++ b/app/data/ct.libs/desktop/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Desktop features", - "tagline": "Currently provides just ct.desktop.quit() method to close a game on desktop", + "tagline": "Currently provides just ct.desktop.quit() method to close a game on desktop.", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/eqs/module.json b/app/data/ct.libs/eqs/module.json index f5860f868..df0fee9f0 100644 --- a/app/data/ct.libs/eqs/module.json +++ b/app/data/ct.libs/eqs/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.eqs", - "tagline": "Environment querying system: apply logic to find the best (or worst) place for positioning", + "tagline": "Aka Environment Querying System. Apply logic to find the best (or worst) place for positioning.", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/fittoscreen/DOCS.md b/app/data/ct.libs/fittoscreen/DOCS.md deleted file mode 100644 index 50858c1c9..000000000 --- a/app/data/ct.libs/fittoscreen/DOCS.md +++ /dev/null @@ -1,25 +0,0 @@ -## `ct.fittoscreen();` - -Resizes the canvas immediately. - -## `ct.fittoscreen.toggleFullscreen();` - -Tries to toggle the fullscreen mode. Errors, if any, will be logged to console. Also, this won't work in the internal ct.js debugger. Instead, test it in your browser. - -This should be called on mouse / keyboard press event, not the "release" event, or the actual transition will happen on the next mouse/keyboard interaction. For example, this will work: - -```js -if (ct.mouse.pressed) { - if (ct.u.prect(ct.mouse.x, ct.mouse.y, this)) { - ct.fittoscreen.toggleFullscreen(); - } -} -``` - -## `ct.fittoscreen.getIsFullscreen();` - -Returns whether the game is in the fullscreen mode (`true`) or not (`false`). - -## `ct.fittoscreen.mode` - -A string that indicates the current scaling approach. Can be changed at runtime to `'fastScale'`, `'expand'`, `'expandViewport'`, `'scaleFit'`, or `'scaleFill'`. \ No newline at end of file diff --git a/app/data/ct.libs/fittoscreen/README.md b/app/data/ct.libs/fittoscreen/README.md index 70d034475..833b535c5 100644 --- a/app/data/ct.libs/fittoscreen/README.md +++ b/app/data/ct.libs/fittoscreen/README.md @@ -1,3 +1,31 @@ +# Fit to Screen + This module allows you to automagically fit your game to screen, either by resizing the whole drawing canvas or by simple scaling. See the settings for this module for different scaling methods. -It also gives you functions to enter a real fullscreen mode programmatically (see the "Reference" tab). \ No newline at end of file +It also gives you functions to enter a real fullscreen mode programmatically (see the "Reference" tab). + +## `ct.fittoscreen();` + +Resizes the canvas immediately. + +## `ct.fittoscreen.toggleFullscreen();` + +Tries to toggle the fullscreen mode. Errors, if any, will be logged to console. Also, this won't work in the internal ct.js debugger. Instead, test it in your browser. + +This should be called on mouse / keyboard press event, not the "release" event, or the actual transition will happen on the next mouse/keyboard interaction. For example, this will work: + +```js +if (ct.mouse.pressed) { + if (ct.u.prect(ct.mouse.x, ct.mouse.y, this)) { + ct.fittoscreen.toggleFullscreen(); + } +} +``` + +## `ct.fittoscreen.getIsFullscreen();` + +Returns whether the game is in the fullscreen mode (`true`) or not (`false`). + +## `ct.fittoscreen.mode` + +A string that indicates the current scaling approach. Can be changed at runtime to `'fastScale'`, `'expand'`, `'expandViewport'`, `'scaleFit'`, or `'scaleFill'`. \ No newline at end of file diff --git a/app/data/ct.libs/fittoscreen/module.json b/app/data/ct.libs/fittoscreen/module.json index 454b80984..46210ec7f 100644 --- a/app/data/ct.libs/fittoscreen/module.json +++ b/app/data/ct.libs/fittoscreen/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Fit to Screen", - "tagline": "An essencial module that manages fullscreen scaling for you", + "tagline": "Do nothing and watch your game adapt to any resolution :)", "version": "4.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/flow/module.json b/app/data/ct.libs/flow/module.json index 451225508..887978bc4 100644 --- a/app/data/ct.libs/flow/module.json +++ b/app/data/ct.libs/flow/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Flow control and timing", - "tagline": "Additional high-level methods to work with asynchronous events, e.g. gate, cumulative delay, retriggerable delay", + "tagline": "Add high-level methods for asynchronous events, e.g. gate, cumulative delay, retriggerable delay.", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/fs/module.json b/app/data/ct.libs/fs/module.json index 3924ff7c7..f6e24e8a0 100644 --- a/app/data/ct.libs/fs/module.json +++ b/app/data/ct.libs/fs/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Desktop's file system", - "tagline": "A file system layer that simplifies Node.js API and lets you focus on your data", + "tagline": "Save and read JSON and text data, scan directories and manage files.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/gamepad/module.json b/app/data/ct.libs/gamepad/module.json index ab11a766a..6e46bf381 100644 --- a/app/data/ct.libs/gamepad/module.json +++ b/app/data/ct.libs/gamepad/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Gamepad", - "tagline": "Provides all the input methods for gamepads", + "tagline": "Connect your gamepad to the Actions system.", "version": "1.0.0", "packageName": "gamepad", "authors": [ diff --git a/app/data/ct.libs/inherit/module.json b/app/data/ct.libs/inherit/module.json index 14721e0a4..45327b9b0 100644 --- a/app/data/ct.libs/inherit/module.json +++ b/app/data/ct.libs/inherit/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Type inheritance", - "tagline": "Works on top of ct.js v1.4 to bring type inheritance and make your code DRY", + "tagline": "Avoid repeating code by adding parent-child relationships between your types.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/keyboard.legacy/module.json b/app/data/ct.libs/keyboard.legacy/module.json index 1718acfd5..8648aeef3 100644 --- a/app/data/ct.libs/keyboard.legacy/module.json +++ b/app/data/ct.libs/keyboard.legacy/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Keyboard", - "tagline": "The old implementation of ct.keyboard that did not support Actions system", + "tagline": "The old implementation of ct.keyboard that did not support Actions system.", "version": "2.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/keyboard.polyfill/README.md b/app/data/ct.libs/keyboard.polyfill/README.md deleted file mode 100644 index ec6920241..000000000 --- a/app/data/ct.libs/keyboard.polyfill/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Allows for new ct.keyboard to work on older browsers. [Source](https://github.com/inexorabletash/polyfill). - -***You should probably disable this module if you are making a desktop release.*** \ No newline at end of file diff --git a/app/data/ct.libs/keyboard.polyfill/module.json b/app/data/ct.libs/keyboard.polyfill/module.json index 5ba4ae05e..d779dba85 100644 --- a/app/data/ct.libs/keyboard.polyfill/module.json +++ b/app/data/ct.libs/keyboard.polyfill/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Keyboard Polyfill", - "tagline": "A polyfill for ct.keyboard so that it works in older browsers", + "tagline": "Make ct.keyboard work with older browsers.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/keyboard/DOCS.md b/app/data/ct.libs/keyboard/DOCS.md deleted file mode 100644 index ea909a62e..000000000 --- a/app/data/ct.libs/keyboard/DOCS.md +++ /dev/null @@ -1,27 +0,0 @@ -## ct.keyboard.lastKey - -Tells the last pressed button. It can be either one of command buttons like `Shift`, `Space`, `Control`, etc., or a digit, or a letter. - -## ct.keyboard.lastCode - -Tells the last pressed button's code. - -## ct.keyboard.string - -Contains text which was written by keyboard. Can be cleared or changed, and it automatically clears on an 'Enter' button. - -## ct.keyboard.alt - -Tells if an `alt` button is held now. - -## ct.keyboard.shift - -Tells if a `shift` button is held now. - -## ct.keyboard.ctrl - -Tells if a `ctrl` button is held now. - -## ct.keyboard.clear(); - -Resets all the parameters listed above. \ No newline at end of file diff --git a/app/data/ct.libs/keyboard/README.md b/app/data/ct.libs/keyboard/README.md index 7793a3994..908731e2d 100644 --- a/app/data/ct.libs/keyboard/README.md +++ b/app/data/ct.libs/keyboard/README.md @@ -1,5 +1,35 @@ +# Keyboard + This module registers all the keyboard events in the game and allows your code to listen button presses and handle inputed strings. This also integrates with `ct.js` action system. Please note that button codes correspond to a physical location of a button on a QWERTY layout, meaning that if you press `q` on QWERTY, `'` on Dvorak or `a` on AZERTY, it will always be `KeyQ`. This will result into a uniform control layout on different keyboard layouts, but will return, for example, non-Dvorak codes if you try to directly write them on screen. Bear that in mind when designing tutorials, tip screens, settings panels, etc. -You can't use input codes to get typed characters. Instead, use `ct.keyboard.string`, which records all the user input, or `ct.keyboard.lastKey`. \ No newline at end of file +You can't use input codes to get typed characters. Instead, use `ct.keyboard.string`, which records all the user input, or `ct.keyboard.lastKey`. + +## ct.keyboard.lastKey + +Tells the last pressed button. It can be either one of command buttons like `Shift`, `Space`, `Control`, etc., or a digit, or a letter. + +## ct.keyboard.lastCode + +Tells the last pressed button's code. + +## ct.keyboard.string + +Contains text which was written by keyboard. Can be cleared or changed, and it automatically clears on an 'Enter' button. + +## ct.keyboard.alt + +Tells if an `alt` button is held now. + +## ct.keyboard.shift + +Tells if a `shift` button is held now. + +## ct.keyboard.ctrl + +Tells if a `ctrl` button is held now. + +## ct.keyboard.clear(); + +Resets all the parameters listed above. \ No newline at end of file diff --git a/app/data/ct.libs/keyboard/module.json b/app/data/ct.libs/keyboard/module.json index 87ecfa8e6..88cdcba43 100644 --- a/app/data/ct.libs/keyboard/module.json +++ b/app/data/ct.libs/keyboard/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Keyboard", - "tagline": "Provides input methods for keyboards", + "tagline": "Add keyboard events to the Actions system.", "version": "3.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/mouse.legacy/module.json b/app/data/ct.libs/mouse.legacy/module.json index 11f2cbb4e..77c461732 100644 --- a/app/data/ct.libs/mouse.legacy/module.json +++ b/app/data/ct.libs/mouse.legacy/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Mouse Input (legacy)", - "tagline": "A deprecated module to listen to mouse events; does not work with Actions system", + "tagline": "A deprecated module to listen to mouse events; does not work with Actions system.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/mouse/DOCS.md b/app/data/ct.libs/mouse/DOCS.md deleted file mode 100644 index 1cc997b4d..000000000 --- a/app/data/ct.libs/mouse/DOCS.md +++ /dev/null @@ -1,64 +0,0 @@ -## `ct.mouse.x`, `ct.mouse.y` - -Current cursor position at horisontal and vertical axes, in game coordinates. - -**Example: make a copy follow the cursor** - -```js -this.x = ct.mouse.x; -this.y = ct.mouse.y; -``` - -# `ct.mouse.xui`, `ct.mouse.yui` - -A cursor position relative to the current view (UI coordinates), but not relative to the room. - -## `ct.mouse.pressed` - -Can be either `true` or `false`. Determines whether a mouse button was pressed. - -**Example: create a bullet on mouse click** - -```js -if (ct.mouse.pressed) { - ct.types.make('Bullet',this.x,this.y); -} -``` - -*You should probably use [Actions](/actions.html) for this case.* - -## `ct.mouse.down` - -Can be either `true` or `false`. Determines whether a mouse button is held down. - -## `ct.mouse.released` - -Can be either `true` or `false`. Determines whether a mouse button was released. - -## `ct.mouse.inside` - -Can be either `true` or `false`. Determines whether there is a cursor inside the drawing canvas. - -## `ct.mouse.hovers(copy)` - -Returns `true` if the mouse hovers over a given `copy` in game coordinates. This does **not** take scaling and rotation into account, as well as polygonal shapes (as they are hollow). - -## `ct.mouse.hoversUi(copy)` - -Returns `true` if the mouse hovers over a given `copy` in UI coordinates. This does **not** take scaling and rotation into account, as well as polygonal shapes (as they are hollow). - -## `ct.mouse.hide()`, `ct.mouse.show()` -Change the visibility of the mouse cursor. - -## Codes for Actions - -* `Left`; -* `Right`; -* `Middle`; -* `Wheel` (alternates between -1, 0 and 1); -* `Special1`; -* `Special2`; -* `Special3`; -* `Special4`; -* `Special5`; -* `Special6`. \ No newline at end of file diff --git a/app/data/ct.libs/mouse/README.md b/app/data/ct.libs/mouse/README.md index 028f567dd..a6facda9f 100644 --- a/app/data/ct.libs/mouse/README.md +++ b/app/data/ct.libs/mouse/README.md @@ -1 +1,68 @@ -This module allows you to listen to mouse events, pipe these events to the Actions system, and get the location of the mouse. \ No newline at end of file +# Mouse Input + +This module allows you to listen to mouse events, pipe these events to the Actions system, and get the location of the mouse. + +## `ct.mouse.x`, `ct.mouse.y` + +Current cursor position at horisontal and vertical axes, in game coordinates. + +**Example: make a copy follow the cursor** + +```js +this.x = ct.mouse.x; +this.y = ct.mouse.y; +``` + +# `ct.mouse.xui`, `ct.mouse.yui` + +A cursor position relative to the current view (UI coordinates), but not relative to the room. + +## `ct.mouse.pressed` + +Can be either `true` or `false`. Determines whether a mouse button was pressed. + +**Example: create a bullet on mouse click** + +```js +if (ct.mouse.pressed) { + ct.types.make('Bullet',this.x,this.y); +} +``` + +*You should probably use [Actions](/actions.html) for this case.* + +## `ct.mouse.down` + +Can be either `true` or `false`. Determines whether a mouse button is held down. + +## `ct.mouse.released` + +Can be either `true` or `false`. Determines whether a mouse button was released. + +## `ct.mouse.inside` + +Can be either `true` or `false`. Determines whether there is a cursor inside the drawing canvas. + +## `ct.mouse.hovers(copy)` + +Returns `true` if the mouse hovers over a given `copy` in game coordinates. This does **not** take scaling and rotation into account, as well as polygonal shapes (as they are hollow). + +## `ct.mouse.hoversUi(copy)` + +Returns `true` if the mouse hovers over a given `copy` in UI coordinates. This does **not** take scaling and rotation into account, as well as polygonal shapes (as they are hollow). + +## `ct.mouse.hide()`, `ct.mouse.show()` +Change the visibility of the mouse cursor. + +## Codes for Actions + +* `Left`; +* `Right`; +* `Middle`; +* `Wheel` (alternates between -1, 0 and 1); +* `Special1`; +* `Special2`; +* `Special3`; +* `Special4`; +* `Special5`; +* `Special6`. \ No newline at end of file diff --git a/app/data/ct.libs/mouse/module.json b/app/data/ct.libs/mouse/module.json index bc8edc474..8772c3c2b 100644 --- a/app/data/ct.libs/mouse/module.json +++ b/app/data/ct.libs/mouse/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Mouse Input", - "tagline": "Provides input methods for mouse", + "tagline": "Get mouse position and read its events in Actions system.", "version": "3.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/place.legacy/module.json b/app/data/ct.libs/place.legacy/module.json index f5cf63cda..9fe373b9d 100644 --- a/app/data/ct.libs/place.legacy/module.json +++ b/app/data/ct.libs/place.legacy/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.place", - "tagline": "Old, unoptimized version of collision library", + "tagline": "Old, unoptimized version of collision library.", "version": "2.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/place/DOCS.md b/app/data/ct.libs/place/docs/Collision checks.md similarity index 67% rename from app/data/ct.libs/place/DOCS.md rename to app/data/ct.libs/place/docs/Collision checks.md index 076f3e8e0..762514698 100644 --- a/app/data/ct.libs/place/DOCS.md +++ b/app/data/ct.libs/place/docs/Collision checks.md @@ -1,3 +1,9 @@ +# Collision checks with `ct.place` + +`ct.place` checks collisions with the methods listed below. Most of the time, it uses a collision group (aka `ctype`) to check against a specific, defined by you, subset of copies, as well as tile layers. After enabling this module, you will find additional fields in type editor and for tile layers in the room editor, where you can set this collision group. + +While runnning a game, you can change a copy's `this.ctype` parameter so `ct.place` detects it under a different collision group. Think of a one-way platform or a barrier that can be turned off. + ## ct.place.free(me, [x, y, ctype]) Checks whether there is a free place at (x;y) for a copy `me`. `ctype` is optional and filters collision by using `ctype` parameter). If `x` and `y` are skipped, the current coordinates of `me` will be used. @@ -19,7 +25,7 @@ If `multiple` is `true`, the function will find all the possible collisions, and ## ct.place.collide(c1, c2) -Returns `true` if there is a collision between `c1` and `c2` Copies. +Returns `true` if there is a collision between `c1` and `c2` Copies. This is rarely used on its own, as it is often more appropriate to use `ct.place.occupied` or `ct.place.meet`. ## ct.place.tile(me, [x, y, ctype]) @@ -31,34 +37,6 @@ If `x` and `y` are skipped, the current coordinates of `me` will be used. This method returns either `true` (a copy collides with a tile layer) or `false` (no collision). -## ct.place.moveAlong(me, direction, maxLength, [ctype, stepSize]) - -Moves a copy by `stepSize` in a given `direction` untill a `maxLength` is reached or a copy is next to another colliding copy. You can filter collided copies with `ctype`, and set precision with `stepSize` (default is `1`, which means pixel-by-pixel movement). This function is especially useful for side-view games and any fast-moving copies, as it allows precise movement without clipping or passing through surfaces. - - -## ct.place.nearest(x, y, type) - -Gets the nearest Copy of a given `type`. - - -## ct.place.furthest(x, y, type) - -Gets the furthest Copy of a given `type`. - - -## ct.place.lastdist - -Returns the latest distance after calling `ct.place.furthest` or `ct.place.nearest`. - - -## ct.place.go(me, x, y, length, [ctype]) - -Tries to reach the target with a simple obstacle avoidance algorithm. - -`me` is a copy to move around, `x` and `y` is a target point; `length` is the maximum amount of pixels to move in one step. `ctype` is an option parameter that tells to test collisions against a certain collision group. - -This function doesn't require the `ct.types.move(this);` call. - ## ct.place.trace(x1, y1, x2, y2, [ctype]) Throws a ray from point (x1, y1) to (x2, y2), returning all the copies that touched the ray. diff --git a/app/data/ct.libs/place/docs/Common pitfalls.md b/app/data/ct.libs/place/docs/Common pitfalls.md new file mode 100644 index 000000000..4b43d6985 --- /dev/null +++ b/app/data/ct.libs/place/docs/Common pitfalls.md @@ -0,0 +1,25 @@ +# Common pitfalls + +## Tiles are always rectangular + +No matter what you define in the Textures tab, `ct.place.tile` will always recognise tiles as rectangles: it greatly increases performance. If you need tiles of other shapes, consider using copies instead of tile layers. + +## Collisions work relative to a copy's parent + +`ct.place` calculates collisions relative to a copy's parent. As UI layers and game layers live in different coordinates, you cannot reliably check collisions between copies of different coordinate spaces. Due to that, use different collision groups for UI elements and gameplay copies. + +## Clipping into copies after transformation + +Rotating or scaling a copy can easily make it "clip into textures". To get out of such sticky situations, you can move your copy away from a collider after executing the movement logic: + +```js +var obstacle = ct.place.occupied(this, 'Solid'), + repelPower = ct.delta * 5; +if (obstacle) { + var repelDirection = ct.u.pdn(obstacle.x, obstacle.y, this.x, this.y); + this.speed = 0; + // Repel the copy from the collider + this.x += this.xprev + ct.u.ldx(repelPower, repelDirection); + this.y += this.yprev + ct.u.ldy(repelPower, repelDirection); +} +``` \ No newline at end of file diff --git a/app/data/ct.libs/place/README.md b/app/data/ct.libs/place/docs/Creating custom collision shapes dynamically.md similarity index 56% rename from app/data/ct.libs/place/README.md rename to app/data/ct.libs/place/docs/Creating custom collision shapes dynamically.md index f9d1625f5..f9a2536c1 100644 --- a/app/data/ct.libs/place/README.md +++ b/app/data/ct.libs/place/docs/Creating custom collision shapes dynamically.md @@ -1,21 +1,6 @@ -Checks collisions between copies. See the "Reference" tab for the methods. +# Creating collision shapes dynamically - -# Collisions in sense of UI and game coordinates - -`ct.place` calculates collisions relative to a copy's parent. As UI layers and game layers live in different coordinates, you cannot reliably check collisions between copies of different coordinate spaces. Due to that, use different collision groups for UI elements and gameplay copies. - - -# Additions to copies - -You can call `this.moveContinuous('CollisionGroup');` at any copy to perform precise movement with collision checks. It takes gravity into account, too, and uses the `ct.place.moveAlong` method. - -Additional field in the type editor "Collision group" defines the `ctype` property used in most ct.place methods. - - -# Preparing types - -> This is useful only when you are going to create copies of different shapes dynamically. In other cases, refer to the Docs tab, as ct.IDE defines these shapes itself. +> This is useful only when you are going to create copies of different shapes dynamically. In other cases, refer to the Collision checks page, as ct.IDE defines these shapes itself. Only these Copies which have a `shape` parameter can collide. Usually these parameters are filled by ct.IDE, but you can change them in-game. For examle, this code will represent a Copy as a circle-shaped object with a 50px radius: @@ -31,7 +16,7 @@ It is important to create a new `shape` object and not to change existing one, b Besides the `shape` parameter, each Copy can have a `ctype` parameter. It is used for grouping Copies into different collision groups, like 'Enemies', 'HeroBullets', 'Obstacles' etc. -# Collision shapes +## Collision shapes * `'point'` — does not require any additional parameters; * `'circle'` — defined by a radius `r`; diff --git a/app/data/ct.libs/place/docs/Movement methods.md b/app/data/ct.libs/place/docs/Movement methods.md new file mode 100644 index 000000000..72921a3bf --- /dev/null +++ b/app/data/ct.libs/place/docs/Movement methods.md @@ -0,0 +1,22 @@ +# Movement methods + +`ct.place` provides two methods for moving copies: `ct.place.moveAlong` for continuous movement and `ct.place.go` for basic collision avoidance. + + +## ct.place.moveAlong(me, direction, maxLength, [ctype, stepSize]) + +Moves a copy by `stepSize` in a given `direction` untill a `maxLength` is reached or a copy is next to another colliding copy. You can filter collided copies with `ctype`, and set precision with `stepSize` (default is `1`, which means pixel-by-pixel movement). This function is especially useful for side-view games and any fast-moving copies, as it allows precise movement without clipping or passing through surfaces. + + +### this.moveContinuous(ctype); + +You can call `this.moveContinuous('CollisionGroup');` at any copy to perform precise movement with collision checks. It takes gravity into account, too, and uses the `ct.place.moveAlong` method. + + +## ct.place.go(me, x, y, length, [ctype]) + +Tries to reach the target with a simple obstacle avoidance algorithm. + +`me` is a copy to move around, `x` and `y` is a target point; `length` is the maximum amount of pixels to move in one step. `ctype` is an option parameter that tells to test collisions against a certain collision group. + +This function doesn't require the `ct.types.move(this);` call. \ No newline at end of file diff --git a/app/data/ct.libs/place/docs/Utilities.md b/app/data/ct.libs/place/docs/Utilities.md new file mode 100644 index 000000000..aa50d3dbd --- /dev/null +++ b/app/data/ct.libs/place/docs/Utilities.md @@ -0,0 +1,13 @@ +# Utilities + +## ct.place.nearest(x, y, type) + +Gets the nearest Copy of a given `type`. + +## ct.place.furthest(x, y, type) + +Gets the furthest Copy of a given `type`. + +## ct.place.lastdist + +Returns the latest distance after calling `ct.place.furthest` or `ct.place.nearest`. diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 2dec36c51..07167e8f3 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.place", - "tagline": "Collision checks, continuous movement, and basic collision avoidance", + "tagline": "Check for collisions, move copies continuously, and add basic collision avoidance to your copies.", "version": "3.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/random/module.json b/app/data/ct.libs/random/module.json index 8c12727c4..d1d4b2f6d 100644 --- a/app/data/ct.libs/random/module.json +++ b/app/data/ct.libs/random/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.random", - "tagline": "A bunch of functions to generate random stuff", + "tagline": "Compute random stuff!", "version": "1.1.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/sound.howler/module.json b/app/data/ct.libs/sound.howler/module.json index e32d222c2..f6cbf8359 100644 --- a/app/data/ct.libs/sound.howler/module.json +++ b/app/data/ct.libs/sound.howler/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.sound.howler", - "tagline": "An advanced sound system for 3D sounds and blending", + "tagline": "Add 3D sounds, sound transitions, load music dynamically and apply effects.", "version": "1.2.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/sprite/module.json b/app/data/ct.libs/sprite/module.json index 30c844ba0..2794a6d8d 100644 --- a/app/data/ct.libs/sprite/module.json +++ b/app/data/ct.libs/sprite/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Sprite Composer", - "tagline": "Creates new animations from one sprite, freeing you from the necessity to split and stitch frames manually", + "tagline": "Create new animations from one sprite, freeing yourself from the necessity to split and stitch frames manually.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/touch/module.json b/app/data/ct.libs/touch/module.json index bdcc3d479..1bf0d367b 100644 --- a/app/data/ct.libs/touch/module.json +++ b/app/data/ct.libs/touch/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Touch", - "tagline": "Input methods for touch devices, plus methods to check on-screen button presses", + "tagline": "Check on-screen button presses and integrate touch events with Actions system.", "version": "2.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/transition/module.json b/app/data/ct.libs/transition/module.json index 57643a8c9..4800eeb04 100644 --- a/app/data/ct.libs/transition/module.json +++ b/app/data/ct.libs/transition/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.transition", - "tagline": "Creates nice and smooth transitions between levels, or when you need them", + "tagline": "Add nice and smooth transitions between levels, or whenever you need them.", "version": "1.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/tween/module.json b/app/data/ct.libs/tween/module.json index 8ccfb2caa..7dd9a3f5b 100644 --- a/app/data/ct.libs/tween/module.json +++ b/app/data/ct.libs/tween/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.tween", - "tagline": "Methods to smoothly animate values through time", + "tagline": "Animate values through time with different interpolation curves.", "version": "0.0.1", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/vkeys/module.json b/app/data/ct.libs/vkeys/module.json index c291f6e6b..8ab761956 100644 --- a/app/data/ct.libs/vkeys/module.json +++ b/app/data/ct.libs/vkeys/module.json @@ -1,7 +1,7 @@ { "main": { "name": "Virtual Keys", - "tagline": "Create on-screen buttons and joysticks and listen to them in Actions system", + "tagline": "Create on-screen buttons and joysticks and listen to them in Actions system.", "version": "2.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/ct.libs/yarn/module.json b/app/data/ct.libs/yarn/module.json index 6286c7b40..428675dde 100644 --- a/app/data/ct.libs/yarn/module.json +++ b/app/data/ct.libs/yarn/module.json @@ -1,7 +1,7 @@ { "main": { "name": "ct.yarn", - "tagline": "Use YarnSpinner projects to create interactive dialogues in your game", + "tagline": "Use YarnSpinner projects to create interactive dialogues in your game.", "version": "0.0.0", "authors": [{ "name": "Cosmo Myzrail Gorynych", diff --git a/app/data/i18n/Brazilian Portuguese.json b/app/data/i18n/Brazilian Portuguese.json index 100555f3a..8590263c6 100644 --- a/app/data/i18n/Brazilian Portuguese.json +++ b/app/data/i18n/Brazilian Portuguese.json @@ -100,7 +100,6 @@ "launch": "Compilar", "license": "Licença", "min": "Modo janela", - "modules": "Catmods", "recentProjects": "Projetos recentes", "rooms": "Salas", "save": "Salvar projeto", @@ -150,8 +149,6 @@ }, "modules": { "author": "Autor deste catmod", - "hasfields": "Configurável", - "hasinjects": "Tem injeções", "help": "Referência", "info": "Info", "license": "Licença", @@ -159,8 +156,7 @@ "methods": "Métodos", "parameters": "Parâmetros", "logs2": "Logs", - "settings": "Configurações ", - "hasinputmethods": "" + "settings": "Configurações " }, "texture": { "create": "Criar", @@ -337,4 +333,4 @@ "docsShortcut": { "openDocs": "" } -} \ No newline at end of file +} diff --git a/app/data/i18n/Chinese Simplified.json b/app/data/i18n/Chinese Simplified.json index e48a6dff2..66596744c 100644 --- a/app/data/i18n/Chinese Simplified.json +++ b/app/data/i18n/Chinese Simplified.json @@ -106,7 +106,6 @@ "launch": "编译并运行", "license": "许可证", "min": "窗口化", - "modules": "Cat模组", "recentProjects": "最近的项目", "rooms": "房间", "save": "保存项目", @@ -175,9 +174,6 @@ }, "modules": { "author": "这个cat模组的作者", - "hasfields": "可配置", - "hasinjects": "有注入", - "hasinputmethods": "提供额外的输入方法", "help": "参考", "info": "信息", "license": "许可证", @@ -396,4 +392,4 @@ "优雅而美丽 🎩" ] } -} \ No newline at end of file +} diff --git a/app/data/i18n/Dutch.json b/app/data/i18n/Dutch.json index 4e975b910..a89647209 100644 --- a/app/data/i18n/Dutch.json +++ b/app/data/i18n/Dutch.json @@ -130,7 +130,6 @@ "launchHotkeys": "(F5; Alt+F5 om in uw standaard browser te draaien)", "license": "License", "min": "Windowed", - "modules": "Catmods", "patrons": "Patrons", "recentProjects": "Recente projecten", "rooms": "Kamers", @@ -229,9 +228,6 @@ "modules": { "author": "Auteru van deze catmod", "dependencies": "Afhankelijkheden:", - "hasfields": "Configurable", - "hasinjects": "Has injections", - "hasinputmethods": "Provides additional input methods", "help": "Reference", "info": "Info", "license": "License", diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 742a7d77c..1753fef5e 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -24,6 +24,7 @@ "exit": "Exit", "exitconfirm": "Are you sure you want to exit?
All the unsaved changes will be lost!", "fastimport": "Fast Import", + "filter": "Filter:", "language": "Language", "loading": "Loading…", "translateToYourLanguage": "Translate ct.js!", @@ -45,6 +46,7 @@ "saveproject": "Save project", "selectDialogue": "Select…", "select": "Select", + "search": "Search:", "sort": "Sort:", "tilelayer": "tile layer", "wrongFormat": "Wrong file format", @@ -82,6 +84,10 @@ "docsShortcut": { "openDocs": "Open the documentation" }, + "docsPanel": { + "documentation": "Documentation", + "reference": "Reference" + }, "exportPanel": { "hide": "Hide", "working": "Working…", @@ -132,7 +138,6 @@ "launchHotkeys": "(F5; Alt+F5 to run in your default browser)", "license": "License", "min": "Windowed", - "modules": "Catmods", "patrons": "Patrons", "recentProjects": "Recent projects", "restart": "Restart", @@ -214,6 +219,9 @@ "iconNotice": "It should be a square, one-frame texture that is at least 256x256px large.", "invertPreloaderScheme": "Invert preloader's color scheme" }, + "modules": { + "heading": "Catmods" + }, "rendering": { "heading": "Render Options", "framerate": "Framerate:", @@ -236,14 +244,12 @@ "moveDown": "Move down", "moveUp": "Move up", "newScriptComment": "Use scripts to define frequent functions and import small libraries" - } + }, + "catmodsSettings": "Catmods' settings" }, "modules": { "author": "Author of this catmod", "dependencies": "Dependencies:", - "hasfields": "Configurable", - "hasinjects": "Has injections", - "hasinputmethods": "Provides additional input methods", "help": "Reference", "info": "Info", "license": "License", @@ -253,7 +259,24 @@ "parameters": "Parameters", "logs2": "Changelog", "settings": "Settings", - "importModules": "Import Modules" + "importModules": "Import Modules", + "enabledModules": "Enabled modules", + "availableModules": "Available modules", + "filter": "Filter", + "categories": { + "customization": "Customization", + "utilities": "Utilities", + "media": "Multimedia", + "misc": "Other", + "desktop": "Desktop builds", + "motionPlanning": "Motion planning", + "inputs": "Input methods", + "fx": "FX", + "mobile": "Mobile devices", + "integrations": "Integrations", + "tweaks": "Tweaks", + "networking": "Networking" + } }, "texture": { "create": "Create", @@ -523,8 +546,9 @@ "notepad": { "local": "Project's notepad", "global": "Global notepad", - "helppages": "Help", - "backToHome": "Back to docs' homepage" + "helppages": "Learn", + "backToHome": "Back to docs' homepage", + "modulespages": "Modules' docs" }, "patreon": { "aboutPatrons": "Patrons are people who show their support to ComigoGames at Patreon, in form of recurring donations. Not everyone there comes from ct.js; some are using other apps from ComigoGames. Tip: if you are a creator and donate to ComigoGames via Patreon, you will get a link to your page here — that's my little help to your creations :)", diff --git a/app/data/i18n/French.json b/app/data/i18n/French.json index d4f28133d..1ae4bd7e0 100644 --- a/app/data/i18n/French.json +++ b/app/data/i18n/French.json @@ -126,7 +126,6 @@ "launchHotkeys":"(F5/ Alt+F5 pour lancer votre navigateur par défaut", "license": "Licence", "min": "Fenêtré", - "modules": "Catmods", "patrons": "Patrons", "recentProjects": "Projets récents", "rooms": "Salons", @@ -217,9 +216,6 @@ }, "modules": { "author": "Auteur de ce CatMod", - "hasfields": "Configurable", - "hasinjects": "A des injections", - "hasinputmethods": "Fournit des méthodes de saisie supplémentaires", "help": "Référence", "info": "Info", "license": "Licence", @@ -518,4 +514,4 @@ "is elegant and beautiful 🎩" ] } -} \ No newline at end of file +} diff --git a/app/data/i18n/German.json b/app/data/i18n/German.json index f0d7d28f2..effbd0733 100755 --- a/app/data/i18n/German.json +++ b/app/data/i18n/German.json @@ -128,7 +128,6 @@ "launchHotkeys": "(F5; Alt+F5 zum Öffnen im Standardbrowser)", "license": "Lizenz", "min": "Fenster", - "modules": "Catmods", "patrons": "Patrons", "recentProjects": "Letzte Projekte", "rooms": "Räume", @@ -213,9 +212,6 @@ }, "modules": { "author": "Autor dieses Catmods", - "hasfields": "Konfigurierbar", - "hasinjects": "Nutzt Injections", - "hasinputmethods": "Bietet zusätzliche Eingabemethoden", "help": "Hilfe", "info": "Info", "license": "Lizenz", @@ -509,4 +505,4 @@ "ist schön und elegant 🎩" ] } -} \ No newline at end of file +} diff --git a/app/data/i18n/Polish.json b/app/data/i18n/Polish.json index ccda050b2..e8a34e6f6 100644 --- a/app/data/i18n/Polish.json +++ b/app/data/i18n/Polish.json @@ -1,407 +1,403 @@ -{ - "me": { - "id": "Pl", - "native": "Polski", - "eng": "Polish" - }, - "common": { - "add": "Dodaj", - "addtonotes": "Dodaj do notatek", - "apply": "Zatwierdź", - "cancel": "Anuluj", - "cannotBeEmpty": "Pole nie może być puste", - "confirmDelete": "Na pewno chcesz usunąć {0}? Nie będzie można tego cofnąć.", - "contribute": "Współpracuj", - "copy": "Kopiuj", - "copyName": "Kopiuj nazwę", - "ctsite": "strona domowa ct.js", - "cut": "Wytnij", - "delete": "Usuń", - "donate": "Wsparcie", - "done": "Gotowe!", - "duplicate": "Powiel", - "exit": "Wyjście", - "exitconfirm": "Na pewno chcesz wyjść?
Wszystkie niezapisane zmiany zostaną utracone!", - "fastimport": "Szybki import", - "language": "Język", - "loading": "Ładowanie…", - "translateToYourLanguage": "Przetłumacz ct.js na swój język!", - "name": "Nazwa:", - "nametaken": "Ta nazwa jest już zajęta", - "newname": "Nowa nazwa:", - "no": "Nie", - "none": "Brak", - "norooms": "Potrzebujesz przynajmniej jednego pokoju żeby skompilować swoją aplikację.", - "notfoundorunknown": "Nieznany plik. Upewnij się, że plik rzeczywiście istnieje", - "ok": "Ok", - "open": "Otwórz", - "openproject": "Otwórz projekt...", - "paste": "Wklej", - "reallyexit": "Na pewno chcesz wyjść? Wszystkie niezapisane zmiany zostaną utracone.", - "rename": "Zmień nazwę", - "save": "Zapisz", - "savedcomm": "Twój projekt został zapisany.", - "saveproject": "Zapisz projekt", - "sort": "Sortuj:", - "tilelayer": "warstwa kafelków", - "wrongFormat": "Zły format pliku", - "yes": "Tak", - "zoom": "Powiększenie", - "zoomIn": "Powiększanie", - "zoomOut": "Pomniejszanie" - }, - "colorPicker": { - "current": "Nowy", - "globalPalette": "Paleta Globalna", - "old": "Stary", - "projectPalette": "Paleta Projektu" - }, - "docsShortcut": { - "openDocs": "Otwórz dokumentację" - }, - "exportPanel": { - "hide": "Schowaj", - "working": "Pracuję…", - "debug": "Wersja debug", - "export": "Eksport", - "exportPanel": "Eksportuj Projekt", - "firstrunnotice": "Pierwsze uruchomienie dla każdej platformy będzie wolne, ponieważ ct.js będzie pobierać i zapisywać dodatkowe biblioteki wymagane do spakowania gry. Może to zająć trochę czasu, ale następne razy będą nimal natychmiastowe.", - "log": "Log Wiadomości" - }, - "intro": { - "loading": "Please wait: kittens are gathering speed of light!", - "newProject": { - "button": "Stwórz", - "input": "Nazwa projektu (litery i cyfry)", - "text": "Stwórz nowy", - "nameerr": "Zła nazwa projektu", - "saveProjectHere": "Zapisz projekt tutaj", - "selectProjectFolder": "Wybierz folder, w którym chcesz przechowywać swój projekt" - }, - "recovery": { - "message": "

Odzyskiwanie

ct.js znalazł plik odzyskiwania. Możliwe, że twój projekt nie został zapisany prawidłowo lub ct.js został zamknięty awaryjnie. Oto kiedy te pliki były ostatnio modyfikowane:

Twój wybrany plik: {0} {1}
Plik odzyskiwania: {2} {3}

Który plik powinien otworzyć ct.js?

", - "loadTarget": "Plik Docelowy", - "loadRecovery": "Odzyskaj", - "newer": "(nowszy)", - "older": "(starszy)" - }, - "loadingProject": "Ładowanie projektu…", - "loadingProjectError": "Nie można otworzyć tego projektu z następującego powodu: ", - "homepage": "Strona domowa", - "latestVersion": "Dostępna jest wersja $1", - "forgetProject": "Zapomnij o tym projekcie", - "browse": "Przeglądaj", - "latest": "Ostatnie projekty", - "unableToWriteToFolders": "Ct.js nie mógł znaleźć odpowiedniego miejsca na projekty! Upewnij się, że przechowujesz aplikację ct.js w folderze, do którego masz prawa zapisu.", - "twitter": "Kanał na Twitterze", - "discord": "Społeczność na Discordzie" - }, - "licensepanel": { - "ctjslicense": "Licencja Ct.js (MIT)" - }, - "menu": { - "ctIDE": "ct.IDE", - "exportDesktop": "Eksportuj na komputer", - "texture": "Tekstury", - "launch": "Kompiluj i uruchom", - "launchHotkeys": "(F5; Alt+F5 żeby uruchomić w twojej domyślnej przeglądarce)", - "license": "Licencja", - "min": "W oknie", - "modules": "Catmody", - "patrons": "Patroni", - "recentProjects": "Ostatnie projekty", - "rooms": "Pokoje", - "save": "Zapisz projekt", - "startScreen": "Powróć do ekranu początkowego", - "settings": "Ustawienia", - "sounds": "Dźwięki", - "successZipExport": "Pomyślnie eksportowano do {0}.", - "successZipProject": "Pomyślnie spakowano projekt do {0}.", - "ui": "UI", - "theme": "Motyw", - "themeDay": "Jasny", - "themeNight": "Ciemny", - "types": "Typy", - "zipExport": "Eksportuj na przeglądarkę", - "zipProject": "Spakuj projekt do .zip", - "codeFontDefault": "Domyślny (Iosevka Light)", - "codeFontOldSchool": "Old school", - "codeFontSystem": "System", - "codeFontCustom": "Dostosuj…", - "newFont": "Nowa czcionka:", - "codeFont": "Czcionka dla kodu", - "codeLigatures": "Ligatury", - "codeDense": "Gęsty układ", - "openIncludeFolder": "Otwórz folder \"include\"" - }, - "onboarding": { - "hoorayHeader": "Wow! Właśnie utworzyłeś projekt!", - "nowWhatParagraph": "Co dalej powinniśmy zrobić?", - "openSpaceShooterTutorial": "Dowiedz się jak zrobić kosmiczną strzelankę", - "openPlatformerTutorial": "Dowiedz się jak zrobić platformówkę", - "doNothing": "Pomiń ten ekran", - "showOnboardingCheckbox": "Pokaż ten ekran przy tworzeniu nowego projektu" - }, - "settings": { - "actions": { - "heading": "Akcje i metody wejścia", - "actions": "Akcje", - "addAction": "Dodaj akcję", - "addMethod": "Dodaj metodę wejścia", - "deleteAction": "Usuń tę akcję", - "deleteMethod": "Usuń tę metodę", - "inputActionNamePlaceholder": "Nazwa akcji", - "methodModuleMissing": "Brak modułu potrzebnego tej metodzie", - "methods": "Metody wejścia", - "multiplier": "Mnożnik", - "noActionsYet": "Akcje pozwalają deweloperom na nasłuchiwanie licznych metod wejścia jednocześnie i na dynamiczną ich zmianę, wszystko za pomocą jednolitego API. Przeczytaj więcej klikając ikonę dokumentu na górze." - }, - "authoring": { - "heading": "Authoring", - "author": "Autor:", - "site": "Strona domowa:", - "title": "Nazwa:", - "version": "Wersja:", - "versionpostfix": "Postfiks:" - }, - "rendering": { - "heading": "Opcje renderowania", - "framerate": "Klatki na sekundę:", - "highDensity": "Wspieraj dużą gęstość pikseli (np. na wyświetlaczach retina)", - "maxFPS": "Maksymalna ilość klatek na sekundę:", - "usePixiLegacy": "Dodaj szeroko wspierany, bazujący na canvasie renderer żeby wspierać stare przeglądarki i karty graficzne (dodaje ~20kb do twojej gry)", - "pixelatedrender": "Zablokuj wygładzanie obrazu tu i w eksportowanym projekcie (zachowaj ostre piksele)" - }, - "scripts": { - "addNew": "Dodaj nowy skrypt", - "deleteScript": "Usuń skrypt", - "heading": "Skrypty", - "newScriptComment": "Uzyj skryptów do definiowania często używanych funkcji i importowania małych bibliotek", - "moveUp": "Przechodzenie w górę", - "moveDown": "Przechodzenie w dół" - } - }, - "modules": { - "author": "Autor tego catmodu", - "hasfields": "Konfigurowalny", - "hasinjects": "Posiada injecty", - "hasinputmethods": "Zapewnia dodatkowe metody wejścia", - "help": "Odnośnik", - "info": "Informacje", - "license": "Licencja", - "logs": "Changelog", - "methods": "Metody", - "parameters": "Parametry", - "logs2": "Changelog", - "settings": "Ustawienia", - "importModules": "Importuj moduły" - }, - "texture": { - "create": "Stwórz", - "import": "Importuj", - "skeletons": "Animacja szkieletowa" - }, - "textureview": { - "bgcolor": "Zmień kolor tła", - "center": "Oś:", - "cols": "Kolumny:", - "done": "Zastosuj", - "fill": "Wypełnij", - "form": "Kształt kolizji:", - "frames": "Liczba klatek:", - "isometrify": "Izometryfikuj: przenieś oś na dół i na środek, wypełnij cały sprite maską kolizji", - "name": "Nazwa:", - "radius": "Promień:", - "rectangle": "Prostokąt", - "reimport": "Importuj ponownie", - "round": "Koło", - "rows": "Rzędy:", - "setcenter": "Środek obrazu", - "speed": "Klatki na sekundę:", - "tiled": "Użyć jako tło?", - "replacetexture": "Zamień…", - "corrupted": "Nie można otworzyć pliku! Zamykanie.", - "showmask": "Pokaż maskę", - "width": "Szerokość:", - "height": "Wysokość:", - "marginx": "Margines X:", - "marginy": "Margines Y:", - "offx": "Odstęp X:", - "offy": "Odstęp Y:", - "strip": "Łamana / Wielokąt", - "removePoint": "Usuń punkt", - "closeShape": "Zamknij kształt", - "addPoint": "Dodaj punkt", - "moveCenter": "Przesuń oś", - "movePoint": "Przesuń ten punkt", - "symmetryTool": "Narzędzie symetrii" - }, - "sounds": { - "create": "Stwórz" - }, - "soundview": { - "import": "Importuj", - "name": "Nazwa:", - "save": "Zapisz", - "isMusicFile": "To jest ścieżka dźwiękowa", - "poolSize": "Rozmiar puli:" - }, - "styles": { - "create": "Stwórz", - "styles": "Style tekstu" - }, - "styleview": { - "active": "Aktywny", - "alignment": "Dopasowanie:", - "apply": "Zastosuj", - "fill": "Wypełnij", - "fillcolor": "Kolor:", - "fillcolor1": "Kolor 1:", - "fillcolor2": "Kolor 2:", - "fillgrad": "Gradient", - "fillgradtype": "Typ gradientu:", - "fillhorisontal": "Poziomy", - "fillsolid": "Rozmycie", - "filltype": "Typ wypełnienia:", - "fillvertical": "Pionowy", - "font": "Czcionka", - "fontweight": "Grubość:", - "fontsize": "Rozmiar czcionki:", - "fontfamily": "Rodzina czcionek:", - "italic": "Pochyła", - "lineHeight": "Wysokość linii:", - "shadow": "Cień", - "shadowblur": "Blur:", - "shadowcolor": "Kolor cienia:", - "shadowshift": "Przesunięcie:", - "stroke": "Przekreślenie", - "strokecolor": "Kolor przekreślenia:", - "strokeweight": "Grubość linii:", - "testtext": "Testowy tekst 0123 +", - "textWrap": "Zawijanie tekstu", - "textWrapWidth": "Maksymalna szerokość:" - }, - "fonts": { - "fonts": "Czcionki", - "import": "Importuj TTF", - "italic": "Pochyła" - }, - "fontview": { - "typefacename": "Nazwa typeface:", - "fontweight": "Grubość czcionki:", - "italic": "Czy jest pochyła?", - "reimport": "Importuj ponownie" - }, - "types": { - "create": "Stwórz" - }, - "typeview": { - "change": "Zmień sprite", - "create": "Przy utworzeniu", - "depth": "Głębia:", - "destroy": "Przy zniszczeniu", - "done": "Gotowe", - "draw": "Rysuj", - "learnAboutTypes": "Dowiedz się więcej o typach kodowania", - "name": "Nazwa:", - "step": "Przy kroku" - }, - "rooms": { - "create": "Dodaj nowy", - "makestarting": "Ustaw jako pokój startowy" - }, - "roombackgrounds": { - "add": "Dodaj tło", - "depth": "Głębia:", - "movement": "Szybkość poruszania się (X, Y):", - "parallax": "Paralaksa (X, Y):", - "repeat": "Powtarzanie:", - "scale": "Skalowanie (X, Y):", - "shift": "Przesunięcie (X, Y):" - }, - "roomtiles": { - "moveTileLayer": "Przenieś na nową głębię", - "show": "Pokaż warstwę", - "hide": "Schowaj warstwę", - "findTileset": "Znajdź tileset" - }, - "roomview": { - "name": "Nazwa:", - "width": "Pokaż szerokość:", - "height": "Wysokość widoku:", - "events": "Zdarzenia pokoju", - "copies": "Kopie", - "backgrounds": "Tła", - "tiles": "Kafelki", - "add": "Dodaj", - "none": "Nic", - "done": "Gotowe", - "grid": "Ustaw siatkę", - "gridoff": "Zablokuj siatkę", - "gridsize": "Rozmiar siatki:", - "hotkeysNotice": "Ctrl = Usuń, Alt = Brak siatki, Shift = Wielokrotnie", - "hotkeysNoticeMovement": "Ctrl = Usuń, Shift = Wybierz", - "tocenter": "Na środek", - "selectbg": "Wybierz tileset", - "shift": "Przesuń widok", - "shifttext": "Przesuń o:", - "step": "Przy kroku", - "create": "Prze utworzeniu", - "leave": "Przy opuszczeniu", - "draw": "Rysuj", - "newdepth": "Nowa głębia:", - "deletecopy": "Usuń kopię {0}", - "deleteCopies": "Usuń kopie", - "shiftCopies": "Przesuń kopie", - "selectAndMove": "Wybierz i przemieść", - "changecopyscale": "Zmień skalę", - "shiftcopy": "Ustaw współrzędne", - "deletetile": "Usuń kafelek", - "deletetiles": "Usuń kafelki", - "movetilestolayer": "Przenieś do warstwy", - "shifttiles": "Przesuń kafelki", - "findTileset": "Znajdź tileset" - }, - "notepad": { - "local": "Notatnik projektu", - "global": "Globalny notatnik", - "helppages": "Pomoc", - "backToHome": "Wróć na stronę domową dokumentacji" - }, - "patreon": { - "aboutPatrons": "Patroni to ludzie, którzy okazują swoje wsparcie dla ComigoGames na Patreonie w formie powtarzających się wpłat. Nie każdy tam pochodzi z ct.js; niektórzy używają innych aplikacji od ComigoGames. Porada: jeśli jesteś twórcą i wesprzesz ComigoGames na Patreonie, dostaniesz link na swoją stonę tutaj - to moja mała pomoc dla twoich dzieł :)", - "patronsHeader": "Nasi patroni", - "businessShuttles": "Promy Biznesowe", - "noShuttlesYet": "Brak Promów Biznesowych :c Twoja firma może być pierwsza!", - "shuttlesDescription": "Promy Biznesowe są uważane za partnerów ct.js. Są wypisane na stronie domowej ct.js' i podstronach sklepu.", - "spacePirates": "Kosmiczni Piraci", - "noPiratesYet": "Brak Kosmicznych Piratów :c", - "piratesDescription": "Kosmiczni Piraci mają priorytetowe wsparcie na serwerze na Discordzie, fajną rolę, oraz są wypisani tutaj", - "spaceProgrammers": "Kosmiczni Programiści", - "programmersDescription": "\"Kosmiczny Programista\" to stara rola, która była dostępna zanim ct.js stał się open-source i dał źródło gier z jamów patronom.", - "aspiringAstronauts": "Aspirujący Astronauci", - "noAstronautsYet": "Brak Aspirujących Astronautów :c", - "astronautsDescription": "Astronauci dostają specjalną rolę na Discordzie i są tu wypisani!", - "thankAllPatrons": "Dziękujemy wszystkim patronom ComigoGames, aktualnym i przyszłym, dzięki waszemu wsparciu Comigo posuwa się naprzód i tworzy jeszcze lepsze aplikacje! :)", - "becomeAPatron": "Zostań patronem", - "aboutFillers": [ - "jest fajny 😎", - "miło z nim porozmawiać 🤗", - "zostanie gwiazdą 💫", - "jest niezwykle uzdolniony ⭐️", - "jest dobrym przyjacielem 🤝", - "jest niezawodny 🙏", - "ma złote serce 🧡", - "jest czarodziejem 🔮", - "jest tu żeby pomóc! 💪", - "jest superbohaterem 🦸‍", - "jeszcze się pojawi 🦹‍", - "jest nierozwiązaną tajemnicą 🔍", - "jest epicki! ✨", - "prawdopodobnie jest robotem 🤖", - "jest jak płonący ogień! 🔥", - "wprowadza światło i nadzieję 🌞", - "jest elegancki i piękny 🎩" - ] - } -} \ No newline at end of file +{ + "me": { + "id": "Pl", + "native": "Polski", + "eng": "Polish" + }, + "common": { + "add": "Dodaj", + "addtonotes": "Dodaj do notatek", + "apply": "Zatwierdź", + "cancel": "Anuluj", + "cannotBeEmpty": "Pole nie może być puste", + "confirmDelete": "Na pewno chcesz usunąć {0}? Nie będzie można tego cofnąć.", + "contribute": "Współpracuj", + "copy": "Kopiuj", + "copyName": "Kopiuj nazwę", + "ctsite": "strona domowa ct.js", + "cut": "Wytnij", + "delete": "Usuń", + "donate": "Wsparcie", + "done": "Gotowe!", + "duplicate": "Powiel", + "exit": "Wyjście", + "exitconfirm": "Na pewno chcesz wyjść?
Wszystkie niezapisane zmiany zostaną utracone!", + "fastimport": "Szybki import", + "language": "Język", + "loading": "Ładowanie…", + "translateToYourLanguage": "Przetłumacz ct.js na swój język!", + "name": "Nazwa:", + "nametaken": "Ta nazwa jest już zajęta", + "newname": "Nowa nazwa:", + "no": "Nie", + "none": "Brak", + "norooms": "Potrzebujesz przynajmniej jednego pokoju żeby skompilować swoją aplikację.", + "notfoundorunknown": "Nieznany plik. Upewnij się, że plik rzeczywiście istnieje", + "ok": "Ok", + "open": "Otwórz", + "openproject": "Otwórz projekt...", + "paste": "Wklej", + "reallyexit": "Na pewno chcesz wyjść? Wszystkie niezapisane zmiany zostaną utracone.", + "rename": "Zmień nazwę", + "save": "Zapisz", + "savedcomm": "Twój projekt został zapisany.", + "saveproject": "Zapisz projekt", + "sort": "Sortuj:", + "tilelayer": "warstwa kafelków", + "wrongFormat": "Zły format pliku", + "yes": "Tak", + "zoom": "Powiększenie", + "zoomIn": "Powiększanie", + "zoomOut": "Pomniejszanie" + }, + "colorPicker": { + "current": "Nowy", + "globalPalette": "Paleta Globalna", + "old": "Stary", + "projectPalette": "Paleta Projektu" + }, + "docsShortcut": { + "openDocs": "Otwórz dokumentację" + }, + "exportPanel": { + "hide": "Schowaj", + "working": "Pracuję…", + "debug": "Wersja debug", + "export": "Eksport", + "exportPanel": "Eksportuj Projekt", + "firstrunnotice": "Pierwsze uruchomienie dla każdej platformy będzie wolne, ponieważ ct.js będzie pobierać i zapisywać dodatkowe biblioteki wymagane do spakowania gry. Może to zająć trochę czasu, ale następne razy będą nimal natychmiastowe.", + "log": "Log Wiadomości" + }, + "intro": { + "loading": "Please wait: kittens are gathering speed of light!", + "newProject": { + "button": "Stwórz", + "input": "Nazwa projektu (litery i cyfry)", + "text": "Stwórz nowy", + "nameerr": "Zła nazwa projektu", + "saveProjectHere": "Zapisz projekt tutaj", + "selectProjectFolder": "Wybierz folder, w którym chcesz przechowywać swój projekt" + }, + "recovery": { + "message": "

Odzyskiwanie

ct.js znalazł plik odzyskiwania. Możliwe, że twój projekt nie został zapisany prawidłowo lub ct.js został zamknięty awaryjnie. Oto kiedy te pliki były ostatnio modyfikowane:

Twój wybrany plik: {0} {1}
Plik odzyskiwania: {2} {3}

Który plik powinien otworzyć ct.js?

", + "loadTarget": "Plik Docelowy", + "loadRecovery": "Odzyskaj", + "newer": "(nowszy)", + "older": "(starszy)" + }, + "loadingProject": "Ładowanie projektu…", + "loadingProjectError": "Nie można otworzyć tego projektu z następującego powodu: ", + "homepage": "Strona domowa", + "latestVersion": "Dostępna jest wersja $1", + "forgetProject": "Zapomnij o tym projekcie", + "browse": "Przeglądaj", + "latest": "Ostatnie projekty", + "unableToWriteToFolders": "Ct.js nie mógł znaleźć odpowiedniego miejsca na projekty! Upewnij się, że przechowujesz aplikację ct.js w folderze, do którego masz prawa zapisu.", + "twitter": "Kanał na Twitterze", + "discord": "Społeczność na Discordzie" + }, + "licensepanel": { + "ctjslicense": "Licencja Ct.js (MIT)" + }, + "menu": { + "ctIDE": "ct.IDE", + "exportDesktop": "Eksportuj na komputer", + "texture": "Tekstury", + "launch": "Kompiluj i uruchom", + "launchHotkeys": "(F5; Alt+F5 żeby uruchomić w twojej domyślnej przeglądarce)", + "license": "Licencja", + "min": "W oknie", + "patrons": "Patroni", + "recentProjects": "Ostatnie projekty", + "rooms": "Pokoje", + "save": "Zapisz projekt", + "startScreen": "Powróć do ekranu początkowego", + "settings": "Ustawienia", + "sounds": "Dźwięki", + "successZipExport": "Pomyślnie eksportowano do {0}.", + "successZipProject": "Pomyślnie spakowano projekt do {0}.", + "ui": "UI", + "theme": "Motyw", + "themeDay": "Jasny", + "themeNight": "Ciemny", + "types": "Typy", + "zipExport": "Eksportuj na przeglądarkę", + "zipProject": "Spakuj projekt do .zip", + "codeFontDefault": "Domyślny (Iosevka Light)", + "codeFontOldSchool": "Old school", + "codeFontSystem": "System", + "codeFontCustom": "Dostosuj…", + "newFont": "Nowa czcionka:", + "codeFont": "Czcionka dla kodu", + "codeLigatures": "Ligatury", + "codeDense": "Gęsty układ", + "openIncludeFolder": "Otwórz folder \"include\"" + }, + "onboarding": { + "hoorayHeader": "Wow! Właśnie utworzyłeś projekt!", + "nowWhatParagraph": "Co dalej powinniśmy zrobić?", + "openSpaceShooterTutorial": "Dowiedz się jak zrobić kosmiczną strzelankę", + "openPlatformerTutorial": "Dowiedz się jak zrobić platformówkę", + "doNothing": "Pomiń ten ekran", + "showOnboardingCheckbox": "Pokaż ten ekran przy tworzeniu nowego projektu" + }, + "settings": { + "actions": { + "heading": "Akcje i metody wejścia", + "actions": "Akcje", + "addAction": "Dodaj akcję", + "addMethod": "Dodaj metodę wejścia", + "deleteAction": "Usuń tę akcję", + "deleteMethod": "Usuń tę metodę", + "inputActionNamePlaceholder": "Nazwa akcji", + "methodModuleMissing": "Brak modułu potrzebnego tej metodzie", + "methods": "Metody wejścia", + "multiplier": "Mnożnik", + "noActionsYet": "Akcje pozwalają deweloperom na nasłuchiwanie licznych metod wejścia jednocześnie i na dynamiczną ich zmianę, wszystko za pomocą jednolitego API. Przeczytaj więcej klikając ikonę dokumentu na górze." + }, + "authoring": { + "heading": "Authoring", + "author": "Autor:", + "site": "Strona domowa:", + "title": "Nazwa:", + "version": "Wersja:", + "versionpostfix": "Postfiks:" + }, + "rendering": { + "heading": "Opcje renderowania", + "framerate": "Klatki na sekundę:", + "highDensity": "Wspieraj dużą gęstość pikseli (np. na wyświetlaczach retina)", + "maxFPS": "Maksymalna ilość klatek na sekundę:", + "usePixiLegacy": "Dodaj szeroko wspierany, bazujący na canvasie renderer żeby wspierać stare przeglądarki i karty graficzne (dodaje ~20kb do twojej gry)", + "pixelatedrender": "Zablokuj wygładzanie obrazu tu i w eksportowanym projekcie (zachowaj ostre piksele)" + }, + "scripts": { + "addNew": "Dodaj nowy skrypt", + "deleteScript": "Usuń skrypt", + "heading": "Skrypty", + "newScriptComment": "Uzyj skryptów do definiowania często używanych funkcji i importowania małych bibliotek", + "moveUp": "Przechodzenie w górę", + "moveDown": "Przechodzenie w dół" + } + }, + "modules": { + "author": "Autor tego catmodu", + "help": "Odnośnik", + "info": "Informacje", + "license": "Licencja", + "logs": "Changelog", + "methods": "Metody", + "parameters": "Parametry", + "logs2": "Changelog", + "settings": "Ustawienia", + "importModules": "Importuj moduły" + }, + "texture": { + "create": "Stwórz", + "import": "Importuj", + "skeletons": "Animacja szkieletowa" + }, + "textureview": { + "bgcolor": "Zmień kolor tła", + "center": "Oś:", + "cols": "Kolumny:", + "done": "Zastosuj", + "fill": "Wypełnij", + "form": "Kształt kolizji:", + "frames": "Liczba klatek:", + "isometrify": "Izometryfikuj: przenieś oś na dół i na środek, wypełnij cały sprite maską kolizji", + "name": "Nazwa:", + "radius": "Promień:", + "rectangle": "Prostokąt", + "reimport": "Importuj ponownie", + "round": "Koło", + "rows": "Rzędy:", + "setcenter": "Środek obrazu", + "speed": "Klatki na sekundę:", + "tiled": "Użyć jako tło?", + "replacetexture": "Zamień…", + "corrupted": "Nie można otworzyć pliku! Zamykanie.", + "showmask": "Pokaż maskę", + "width": "Szerokość:", + "height": "Wysokość:", + "marginx": "Margines X:", + "marginy": "Margines Y:", + "offx": "Odstęp X:", + "offy": "Odstęp Y:", + "strip": "Łamana / Wielokąt", + "removePoint": "Usuń punkt", + "closeShape": "Zamknij kształt", + "addPoint": "Dodaj punkt", + "moveCenter": "Przesuń oś", + "movePoint": "Przesuń ten punkt", + "symmetryTool": "Narzędzie symetrii" + }, + "sounds": { + "create": "Stwórz" + }, + "soundview": { + "import": "Importuj", + "name": "Nazwa:", + "save": "Zapisz", + "isMusicFile": "To jest ścieżka dźwiękowa", + "poolSize": "Rozmiar puli:" + }, + "styles": { + "create": "Stwórz", + "styles": "Style tekstu" + }, + "styleview": { + "active": "Aktywny", + "alignment": "Dopasowanie:", + "apply": "Zastosuj", + "fill": "Wypełnij", + "fillcolor": "Kolor:", + "fillcolor1": "Kolor 1:", + "fillcolor2": "Kolor 2:", + "fillgrad": "Gradient", + "fillgradtype": "Typ gradientu:", + "fillhorisontal": "Poziomy", + "fillsolid": "Rozmycie", + "filltype": "Typ wypełnienia:", + "fillvertical": "Pionowy", + "font": "Czcionka", + "fontweight": "Grubość:", + "fontsize": "Rozmiar czcionki:", + "fontfamily": "Rodzina czcionek:", + "italic": "Pochyła", + "lineHeight": "Wysokość linii:", + "shadow": "Cień", + "shadowblur": "Blur:", + "shadowcolor": "Kolor cienia:", + "shadowshift": "Przesunięcie:", + "stroke": "Przekreślenie", + "strokecolor": "Kolor przekreślenia:", + "strokeweight": "Grubość linii:", + "testtext": "Testowy tekst 0123 +", + "textWrap": "Zawijanie tekstu", + "textWrapWidth": "Maksymalna szerokość:" + }, + "fonts": { + "fonts": "Czcionki", + "import": "Importuj TTF", + "italic": "Pochyła" + }, + "fontview": { + "typefacename": "Nazwa typeface:", + "fontweight": "Grubość czcionki:", + "italic": "Czy jest pochyła?", + "reimport": "Importuj ponownie" + }, + "types": { + "create": "Stwórz" + }, + "typeview": { + "change": "Zmień sprite", + "create": "Przy utworzeniu", + "depth": "Głębia:", + "destroy": "Przy zniszczeniu", + "done": "Gotowe", + "draw": "Rysuj", + "learnAboutTypes": "Dowiedz się więcej o typach kodowania", + "name": "Nazwa:", + "step": "Przy kroku" + }, + "rooms": { + "create": "Dodaj nowy", + "makestarting": "Ustaw jako pokój startowy" + }, + "roombackgrounds": { + "add": "Dodaj tło", + "depth": "Głębia:", + "movement": "Szybkość poruszania się (X, Y):", + "parallax": "Paralaksa (X, Y):", + "repeat": "Powtarzanie:", + "scale": "Skalowanie (X, Y):", + "shift": "Przesunięcie (X, Y):" + }, + "roomtiles": { + "moveTileLayer": "Przenieś na nową głębię", + "show": "Pokaż warstwę", + "hide": "Schowaj warstwę", + "findTileset": "Znajdź tileset" + }, + "roomview": { + "name": "Nazwa:", + "width": "Pokaż szerokość:", + "height": "Wysokość widoku:", + "events": "Zdarzenia pokoju", + "copies": "Kopie", + "backgrounds": "Tła", + "tiles": "Kafelki", + "add": "Dodaj", + "none": "Nic", + "done": "Gotowe", + "grid": "Ustaw siatkę", + "gridoff": "Zablokuj siatkę", + "gridsize": "Rozmiar siatki:", + "hotkeysNotice": "Ctrl = Usuń, Alt = Brak siatki, Shift = Wielokrotnie", + "hotkeysNoticeMovement": "Ctrl = Usuń, Shift = Wybierz", + "tocenter": "Na środek", + "selectbg": "Wybierz tileset", + "shift": "Przesuń widok", + "shifttext": "Przesuń o:", + "step": "Przy kroku", + "create": "Prze utworzeniu", + "leave": "Przy opuszczeniu", + "draw": "Rysuj", + "newdepth": "Nowa głębia:", + "deletecopy": "Usuń kopię {0}", + "deleteCopies": "Usuń kopie", + "shiftCopies": "Przesuń kopie", + "selectAndMove": "Wybierz i przemieść", + "changecopyscale": "Zmień skalę", + "shiftcopy": "Ustaw współrzędne", + "deletetile": "Usuń kafelek", + "deletetiles": "Usuń kafelki", + "movetilestolayer": "Przenieś do warstwy", + "shifttiles": "Przesuń kafelki", + "findTileset": "Znajdź tileset" + }, + "notepad": { + "local": "Notatnik projektu", + "global": "Globalny notatnik", + "helppages": "Pomoc", + "backToHome": "Wróć na stronę domową dokumentacji" + }, + "patreon": { + "aboutPatrons": "Patroni to ludzie, którzy okazują swoje wsparcie dla ComigoGames na Patreonie w formie powtarzających się wpłat. Nie każdy tam pochodzi z ct.js; niektórzy używają innych aplikacji od ComigoGames. Porada: jeśli jesteś twórcą i wesprzesz ComigoGames na Patreonie, dostaniesz link na swoją stonę tutaj - to moja mała pomoc dla twoich dzieł :)", + "patronsHeader": "Nasi patroni", + "businessShuttles": "Promy Biznesowe", + "noShuttlesYet": "Brak Promów Biznesowych :c Twoja firma może być pierwsza!", + "shuttlesDescription": "Promy Biznesowe są uważane za partnerów ct.js. Są wypisane na stronie domowej ct.js' i podstronach sklepu.", + "spacePirates": "Kosmiczni Piraci", + "noPiratesYet": "Brak Kosmicznych Piratów :c", + "piratesDescription": "Kosmiczni Piraci mają priorytetowe wsparcie na serwerze na Discordzie, fajną rolę, oraz są wypisani tutaj", + "spaceProgrammers": "Kosmiczni Programiści", + "programmersDescription": "\"Kosmiczny Programista\" to stara rola, która była dostępna zanim ct.js stał się open-source i dał źródło gier z jamów patronom.", + "aspiringAstronauts": "Aspirujący Astronauci", + "noAstronautsYet": "Brak Aspirujących Astronautów :c", + "astronautsDescription": "Astronauci dostają specjalną rolę na Discordzie i są tu wypisani!", + "thankAllPatrons": "Dziękujemy wszystkim patronom ComigoGames, aktualnym i przyszłym, dzięki waszemu wsparciu Comigo posuwa się naprzód i tworzy jeszcze lepsze aplikacje! :)", + "becomeAPatron": "Zostań patronem", + "aboutFillers": [ + "jest fajny 😎", + "miło z nim porozmawiać 🤗", + "zostanie gwiazdą 💫", + "jest niezwykle uzdolniony ⭐️", + "jest dobrym przyjacielem 🤝", + "jest niezawodny 🙏", + "ma złote serce 🧡", + "jest czarodziejem 🔮", + "jest tu żeby pomóc! 💪", + "jest superbohaterem 🦸‍", + "jeszcze się pojawi 🦹‍", + "jest nierozwiązaną tajemnicą 🔍", + "jest epicki! ✨", + "prawdopodobnie jest robotem 🤖", + "jest jak płonący ogień! 🔥", + "wprowadza światło i nadzieję 🌞", + "jest elegancki i piękny 🎩" + ] + } +} diff --git a/app/data/i18n/Romanian.json b/app/data/i18n/Romanian.json index ea019982a..fa28d0c6e 100644 --- a/app/data/i18n/Romanian.json +++ b/app/data/i18n/Romanian.json @@ -103,7 +103,6 @@ "launch": "Compilează", "license": "Licență", "min": "Windowed", - "modules": "Catmod-uri", "recentProjects": "Proiecte recente", "rooms": "Camere", "save": "Salvează proiectul", @@ -166,9 +165,6 @@ }, "modules": { "author": "Autorul acestui catmod", - "hasfields": "Configurabil", - "hasinjects": "Are injecții", - "hasinputmethods": "Furnizează metode de introducere adiționale", "help": "Referință", "info": "Info", "license": "Licență", @@ -350,4 +346,4 @@ "helppages": "Ajutor", "backToHome": "Înapoi la pagina principală a documentației" } -} \ No newline at end of file +} diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index ca0edc8d0..7ad31a450 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -111,7 +111,6 @@ "launchHotkeys": "(F5, или Alt+F5, чтобы запустить в браузере по-умолчанию)", "license": "Лицензия", "min": "Переключить полноэкранный режим", - "modules": "Котомоды", "patrons": "Патроны-покровители", "recentProjects": "Последние проекты", "rooms": "Комнаты", @@ -215,9 +214,6 @@ }, "modules": { "author": "Автор котомода", - "hasfields": "Настраиваемый", - "hasinjects": "Содержит инъекции", - "hasinputmethods": "Добавляет новые методы ввода", "help": "Документация", "methods": "Методы", "parameters": "Параметры", @@ -529,4 +525,4 @@ "openExternal": "Открыть в браузере", "close": "Закрыть" } -} \ No newline at end of file +} diff --git a/app/data/i18n/Spanish.json b/app/data/i18n/Spanish.json index 3dbabe840..a94e88839 100644 --- a/app/data/i18n/Spanish.json +++ b/app/data/i18n/Spanish.json @@ -103,7 +103,6 @@ "launch": "Compilar", "license": "Licencia", "min": "Modo Ventana", - "modules": "Catmods", "recentProjects": "Proyectos recientes", "rooms": "Rooms", "save": "Guardar proyecto", @@ -166,9 +165,6 @@ }, "modules": { "author": "Autor de este catmod", - "hasfields": "Configurable", - "hasinjects": "Tiene inyecciones", - "hasinputmethods": "Proporciona métodos de entrada adicionales.", "help": "Referencia", "info": "Info", "license": "Licencia", @@ -350,4 +346,4 @@ "helppages": "Ayuda", "backToHome": "Volver a la página de inicio de los documentos" } -} \ No newline at end of file +} diff --git a/gulpfile.js b/gulpfile.js index 60b7aa59e..fd0c3601d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -143,6 +143,10 @@ const copyRequires = () => .pipe(sourcemaps.write()) .pipe(gulp.dest('./app/data/node_requires')); +const copyInEditorDocs = () => + gulp.src('./docs/docs/ct.*.md') + .pipe(gulp.dest('./app/data/node_requires')); + const compileScripts = gulp.series(compileRiot, concatScripts); const icons = () => @@ -379,6 +383,7 @@ const build = gulp.parallel([ compileStylus, compileScripts, copyRequires, + copyInEditorDocs, icons, bakeTypedefs ]); diff --git a/src/icons/film.svg b/src/icons/film.svg new file mode 100644 index 000000000..ac46360d2 --- /dev/null +++ b/src/icons/film.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/globe.svg b/src/icons/globe.svg new file mode 100644 index 000000000..0a0586d36 --- /dev/null +++ b/src/icons/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/monitor.svg b/src/icons/monitor.svg new file mode 100644 index 000000000..6c3556db2 --- /dev/null +++ b/src/icons/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/refresh-cw.svg b/src/icons/refresh-cw.svg new file mode 100644 index 000000000..06c358dd0 --- /dev/null +++ b/src/icons/refresh-cw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/tool.svg b/src/icons/tool.svg new file mode 100644 index 000000000..f3cbf3d90 --- /dev/null +++ b/src/icons/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/tv.svg b/src/icons/tv.svg new file mode 100644 index 000000000..955bbfff0 --- /dev/null +++ b/src/icons/tv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/node_requires/resources/modules/index.js b/src/node_requires/resources/modules/index.js new file mode 100644 index 000000000..ea032c44e --- /dev/null +++ b/src/node_requires/resources/modules/index.js @@ -0,0 +1,122 @@ +const moduleDir = './data/ct.libs'; + +/* async */ +const loadModule = dir => { + const fs = require('fs-extra'), + path = require('path'); + return fs.readJSON(dir, path.join(dir, 'module.json')); +}; +/* async */ +const loadModuleByName = name => { + const path = require('path'); + return loadModule(path.join(moduleDir, name)); +}; +const loadModules = async () => { + const fs = require('fs-extra'), + path = require('path'); + + // Reads the modules directory + const files = await fs.readdir(moduleDir); + + const modules = (await Promise.all(files.map(async file => { + // We include only those folders that have `module.json` inside + if (await fs.pathExists(path.join(moduleDir, file, 'module.json'))) { + const moduleName = file; + return { + name: moduleName, + path: path.join(moduleDir, file), + manifest: await fs.readJSON(path.join(moduleDir, file, 'module.json')) + }; + } + return false; + }))).filter(module => module); // Remove `false` results from array + + return modules; +}; + +const getModuleDocStructure = async module => { + const path = require('path'), + fs = require('fs-extra'); + const docStructure = []; + const readmeExists = fs.pathExists(path.join(module.path, 'README.md')), + docsExists = fs.pathExists(path.join(module.path, 'DOCS.md')), + docsStat = fs.lstat(path.join(module.path, 'docs')); + if (await readmeExists) { + docStructure.push({ + vocKey: 'documentation', + path: path.join(module.path, 'README.md') + }); + } + if (await docsExists) { + docStructure.push({ + vocKey: 'reference', + path: path.join(module.path, 'DOCS.md') + }); + } + + try { // Search for a docs folder and add its items + if ((await docsStat).isDirectory()) { + const files = (await fs.readdir(path.join(module.path, 'docs'))) + .filter(file => /\.md$/.test(file)); + for (const file of files) { + docStructure.push({ + name: path.basename(file, path.extname(file)), + path: path.join(module.path, 'docs', file) + }); + } + } + } catch (err) { // No such directory; do nothing + void err; + } + if (docStructure.length) { + // Such a module does not use README.md or DOCS.md, + // but we still need to put a top-level nav item + if (!(await readmeExists) && !(await docsExists)) { + // The name is set right below ⤵ + docStructure.unshift({}); + } + // The first element's name should be named after the module itself. + docStructure[0].name = module.manifest.main.name; + } + return docStructure; +}; + +/** + * A mapping of general module categories to their icons. + */ +const categoryToIconMap = { + customization: 'droplet', + utilities: 'tool', + media: 'tv', + misc: 'loader', + desktop: 'monitor', + motionPlanning: 'move', + inputs: 'airplay', + fx: 'sparkles', + mobile: 'smartphone', + integrations: 'plus-circle', + tweaks: 'sliders', + networking: 'globe', + default: 'ctmod' +}; + +const getIcon = module => { + const {categories} = module.manifest.main; + if (!categories || categories.length === 0) { + return categoryToIconMap.default; + } + if (categories[0] in categoryToIconMap) { + return categoryToIconMap[categories[0]]; + } + return categoryToIconMap.default; +}; + +module.exports = { + loadModule, + loadModuleByName, + loadModules, + moduleDir, + getModuleDocStructure, + categoryToIconMap, + getIcon +}; diff --git a/src/riotTags/docs-panel.tag b/src/riotTags/docs-panel.tag new file mode 100644 index 000000000..1e5597340 --- /dev/null +++ b/src/riotTags/docs-panel.tag @@ -0,0 +1,76 @@ +docs-panel + .flexrow + aside.docs-panel-aNavigation + virtual(each="{docCategory in docs}") + h3( + onclick="{parent.showPage(docCategory.rootNode.path)}" + class="{active: docCategory.rootNode.path && currentPath === docCategory.rootNode.path, a: docCategory.rootNode.path}" + ) + span {docCategory.rootNode.name} + ul(if="{docCategory.childNodes.length}") + li( + each="{node in docCategory.childNodes}" + onclick="{parent.parent.showPage(node.path, node.type)}" + class="{active: currentPath === node.path, a: node.path}" + ) + span(if="{node.name}") {node.name} + span(if="{!node.name}") {voc[node.vocKey]} + .docs-panel-Docs + .docs-panel-DocsWidthLimiter + raw(ref="raw" content="{docContent}") + script. + this.namespace = 'docsPanel'; + this.mixin(window.riotVoc); + + this.docs = []; + this.docContent = ''; + + this.refreshDocs = async () => { + const modules = require('./data/node_requires/resources/modules'); + const catmods = (await modules.loadModules()) + .filter(module => module.name in global.currentProject.libs); + const docOrders = catmods.map(modules.getModuleDocStructure); + const unfilteredDocs = await Promise.all(docOrders); + this.docs = []; + for (const docGroup of unfilteredDocs) { + if (docGroup.length) { + this.docs.push({ + rootNode: docGroup[0], + childNodes: docGroup.slice(1) + }); + } + } + this.update(); + }; + + window.signals.on('modulesChanged', this.refreshDocs); + this.on('unmount', () => { + window.signals.off('modulesChanged', this.refreshDocs); + }); + this.refreshDocs(); + + const md = require('markdown-it')({ + html: false, + linkify: true, + highlight: function highlight(str, lang) { + const hljs = require('highlight.js'); + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(lang, str).value; + } catch (oO) { + void 0; + } + } + return ''; // use external default escaping + } + }); + + this.showPage = path => async () => { + if (!path) { + return; + } + const fs = require('fs-extra'); + this.currentPath = path; + this.docContent = md.render(await fs.readFile(path, 'utf8')); + this.update(); + }; diff --git a/src/riotTags/main-menu.tag b/src/riotTags/main-menu.tag index 57558557f..5dbf60bbd 100644 --- a/src/riotTags/main-menu.tag +++ b/src/riotTags/main-menu.tag @@ -24,10 +24,6 @@ main-menu.flexcol svg.feather use(xlink:href="data/icons.svg#sliders") span {voc.project} - li(onclick="{changeTab('modules')}" class="{active: tab === 'modules'}" data-hotkey="Control+2" title="Control+2") - svg.feather - use(xlink:href="data/icons.svg#ctmod") - span {voc.modules} li(onclick="{changeTab('texture')}" class="{active: tab === 'texture'}" data-hotkey="Control+3" title="Control+3") svg.feather use(xlink:href="data/icons.svg#texture") @@ -55,7 +51,6 @@ main-menu.flexcol div.flexitem.relative(if="{global.currentProject}") debugger-screen-embedded(if="{tab === 'debug'}" params="{debugParams}" data-hotkey-scope="play" ref="debugger") project-settings(show="{tab === 'project'}" data-hotkey-scope="project") - modules-panel(show="{tab === 'modules'}" data-hotkey-scope="modules") textures-panel(show="{tab === 'texture'}" data-hotkey-scope="texture") ui-panel(show="{tab === 'ui'}" data-hotkey-scope="ui") fx-panel(show="{tab === 'fx'}" data-hotkey-scope="fx") @@ -159,16 +154,15 @@ main-menu.flexcol // Run a local server for ct.js games let fileServer; getExportDir().then(dir => { - console.log(dir); const fileServerSettings = { public: dir, cleanUrls: true }; const handler = require('serve-handler'); - fileServer = require('http').createServer((request, response) => { - return handler(request, response, fileServerSettings); - }); + fileServer = require('http').createServer((request, response) => + handler(request, response, fileServerSettings)); fileServer.listen(0, () => { + // eslint-disable-next-line no-console console.info(`[ct.debugger] Running dev server at http://localhost:${fileServer.address().port}`); }); }); @@ -218,6 +212,7 @@ main-menu.flexcol try { const os = require('os'); const path = require('path'); + const {getWritableDir} = require('./data/node_requires/platformUtils'); const writable = await getWritableDir(); const inDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ctZipProject-')), @@ -246,6 +241,7 @@ main-menu.flexcol } }; this.zipExport = async () => { + const {getWritableDir} = require('./data/node_requires/platformUtils'); const writable = await getWritableDir(); const runCtExport = require('./data/node_requires/exporter'); const exportFile = path.join(writable, '/export.zip'), diff --git a/src/riotTags/modules-panel.tag b/src/riotTags/modules-panel.tag deleted file mode 100644 index 208dbdff8..000000000 --- a/src/riotTags/modules-panel.tag +++ /dev/null @@ -1,347 +0,0 @@ -modules-panel.panel.view - .flexrow.tall - .c3.borderright.tall - ul#moduleincluded - li(each="{module in enabledModules}" onclick="{renderModule(module)}") - svg.feather.success - use(xlink:href="data/icons.svg#check") - span {module} - ul#modulelist - li(each="{module in allModules}" onclick="{renderModule(module)}") - svg.feather(class="{(module in global.currentProject.libs)? 'success on' : 'off'}") - use(xlink:href="data/icons.svg#{(module in global.currentProject.libs)? 'check' : 'ctmod'}") - span {module} - label.file.flexfix-footer.nmb - input( - type="file" - ref="importmodules" - accept=".zip" - onchange="{importModules}") - .button.wide.inline.nml.nmr - svg.feather - use(xlink:href="data/icons.svg#folder") - span {voc.importModules} - .c9.tall.flexfix(if="{currentModule}") - ul.nav.tabs.flexfix-header.noshrink - li#modinfo(onclick="{changeTab('moduleinfo')}" class="{active: tab === 'moduleinfo'}") - svg.feather - use(xlink:href="data/icons.svg#info") - span {voc.info} - li#modsettings(if="{currentModule.fields && currentModuleName in global.currentProject.libs}" onclick="{changeTab('modulesettings')}" class="{active: tab === 'modulesettings'}") - svg.feather - use(xlink:href="data/icons.svg#settings") - span {voc.settings} - li#modhelp( if="{currentModuleDocs}" onclick="{changeTab('modulehelp')}" class="{active: tab === 'modulehelp'}") - svg.feather - use(xlink:href="data/icons.svg#file-text") - span {voc.help} - li#modlogs(if="{currentModuleLogs}" onclick="{changeTab('modulelogs')}" class="{active: tab === 'modulelogs'}") - svg.feather - use(xlink:href="data/icons.svg#list") - span {voc.logs} - div.flexfix-body - #moduleinfo.tabbed.nbt(show="{tab === 'moduleinfo'}") - label.bigpower(onclick="{toggleModule(currentModuleName)}" class="{off: !(currentModuleName in global.currentProject.libs)}") - svg.feather - use(xlink:href="data/icons.svg#{currentModuleName in global.currentProject.libs? 'check' : 'x'}") - span - h1#modname - span {currentModule.main.name} - span.version {currentModule.main.version} - a.external(each="{author in currentModule.main.authors}" title="{voc.author}" href="{author.site || 'mailto:'+author.mail}") - svg.feather - use(xlink:href="data/icons.svg#user") - span#modauthor {author.name} - - span(title="{voc.hasinjects}" show="{currentModule.injects}") - svg.feather.warning#modinjects - use(xlink:href="data/icons.svg#zap") - span(title="{voc.hasfields}" show="{currentModule.fields}") - svg.feather.success - use(xlink:href="data/icons.svg#settings") - span(title="{voc.hasinputmethods}" show="{currentModule.inputMethods}") - svg.feather.success - use(xlink:href="data/icons.svg#airplay") - - div(if="{currentModule.dependencies && currentModule.dependencies.length}") - .spacer - b {voc.dependencies} - .inlineblock(each="{dependency in currentModule.dependencies}") - svg.feather(if="{dependency in global.currentProject.libs}").success - use(xlink:href="data/icons.svg#check") - svg.feather(if="{!(dependency in global.currentProject.libs)}").error - use(xlink:href="data/icons.svg#alert-circle") - span {dependency} - - div(if="{currentModule.optionalDependencies && currentModule.optionalDependencies.length}") - .spacer - b {voc.optionalDependencies} - .inlineblock(each="{dependency in currentModule.optionalDependencies}") - svg.feather(if="{dependency in global.currentProject.libs}").success - use(xlink:href="data/icons.svg#check") - svg.feather(if="{!(dependency in global.currentProject.libs)}").warning - use(xlink:href="data/icons.svg#alert-triangle") - span {dependency} - - #modinfohtml(if="{currentModuleHelp}") - raw(ref="raw" content="{currentModuleHelp}") - h1(if="{currentModuleLicense}") {voc.license} - pre(if="{currentModuleLicense}") - code {currentModuleLicense} - - #modulesettings.tabbed.nbt(show="{tab === 'modulesettings'}" if="{currentModule.fields && currentModuleName in global.currentProject.libs}") - extensions-editor(customextends="{currentModule.fields}" entity="{global.currentProject.libs[currentModuleName]}") - #modulehelp.tabbed.nbt(show="{tab === 'modulehelp'}" if="{currentModuleDocs}") - raw(ref="raw" content="{currentModuleDocs}") - #modulelogs.tabbed.nbt(show="{tab === 'modulelogs'}" if="{currentModuleLogs}") - h1 {voc.logs2} - raw(ref="raw" content="{currentModuleLogs}") - - script. - const path = require('path'), - fs = require('fs-extra'), - unzipper = require('unzipper'); - const md = require('markdown-it')({ - html: false, - linkify: true, - highlight: function highlight(str, lang) { - const hljs = require('highlight.js'); - if (lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(lang, str).value; - } catch (oO) { - void 0; - } - } - return ''; // use external default escaping - } - }); - const glob = require('./data/node_requires/glob'); - const libsDir = './data/ct.libs'; - this.md = md; - this.mixin(window.riotWired); - this.namespace = 'modules'; - this.mixin(window.riotVoc); - - this.currentModule = false; - this.currentModuleHelp = ''; - this.currentModuleDocs = ''; - this.currentModuleLicense = ''; - - this.tab = 'moduleinfo'; - this.changeTab = tab => () => { - this.tab = tab; - }; - - this.allModules = []; - this.on('update', () => { - this.enabledModules = []; - for (const i in global.currentProject.libs) { - this.enabledModules.push(i); - } - }); - - this.escapeDots = str => str.replace(/\./g, '\\.'); - - const tryLoadTypedefs = moduleName => { - if (!(moduleName in global.currentProject.libs)) { - return; - } - const typedefPath = path.join(libsDir, moduleName, 'types.d.ts'); - fs.pathExists(typedefPath) - .then(exists => { - const ts = monaco.languages.typescript; - if (!exists) { - // generate dummy typedefs if none were provided by the module - const catmodTypedefs = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${moduleName}: any; }`; - glob.moduleTypings[moduleName] = [ - ts.javascriptDefaults.addExtraLib(catmodTypedefs), - ts.typescriptDefaults.addExtraLib(catmodTypedefs) - ]; - return; - } - fs.readFile(typedefPath, 'utf8') - .then(catmodTypedefs => { - glob.moduleTypings[moduleName] = [ - ts.javascriptDefaults.addExtraLib(catmodTypedefs), - ts.typescriptDefaults.addExtraLib(catmodTypedefs) - ]; - }); - }); - }; - const removeTypedefs = moduleName => { - if (glob.moduleTypings[moduleName]) { - for (const lib of glob.moduleTypings[moduleName]) { - lib.dispose(); - } - } - }; - fs.readdir(libsDir, (err, files) => { - if (err) { - throw err; - } - for (var i = 0; i < files.length; i++) { - if (fs.pathExistsSync(path.join(libsDir, files[i], 'module.json'))) { - const moduleName = files[i]; - this.allModules.push(moduleName); - tryLoadTypedefs(moduleName); - } - } - this.currentModuleHelp = ''; - this.currentModuleDocs = ''; - this.currentModuleLicense = ''; - this.renderModule(this.allModules[0])(); - this.update(); - }); - - const addDefaults = moduleName => { - for (const field of this.currentModule.fields) { - if (!global.currentProject.libs[moduleName][field.key]) { - if (field.default) { - global.currentProject.libs[moduleName][field.key] = field.default; - } else if (field.type === 'number') { - global.currentProject.libs[moduleName][field.key] = 0; - } else if (field.type === 'checkbox') { - global.currentProject.libs[moduleName][field.key] = false; - } else { - global.currentProject.libs[moduleName][field.key] = ''; - } - } - } - }; - - this.toggleModule = moduleName => e => { - if (global.currentProject.libs[moduleName]) { - delete global.currentProject.libs[moduleName]; - removeTypedefs(moduleName); - } else { - global.currentProject.libs[moduleName] = {}; - tryLoadTypedefs(moduleName); - // 'Settings' page - if (this.currentModule.fields) { - addDefaults(moduleName); - } - } - this.renderModule(moduleName)(e); - window.signals.trigger('modulesChanged'); - glob.modified = true; - }; - this.renderModule = name => () => { - fs.readJSON(path.join(libsDir, name, 'module.json'), (err, catmod) => { - if (err) { - alertify.error(err); - if (name in global.currentProject.libs) { - delete global.currentProject.libs[name]; - } - this.currentModule = false; - this.update(); - return; - } - this.currentModule = catmod; - this.currentModuleName = name; - - if (fs.pathExistsSync(path.join(libsDir, name, 'README.md'))) { - this.currentModuleHelp = md.render(fs.readFileSync(path.join(libsDir, name, 'README.md'), { - encoding: 'utf8' - }) || ''); - } else { - this.currentModuleHelp = false; - } - if (fs.pathExistsSync(path.join(libsDir, name, 'DOCS.md'))) { - this.currentModuleDocs = md.render(fs.readFileSync(path.join(libsDir, name, 'DOCS.md'), { - encoding: 'utf8' - }) || ''); - } else { - this.currentModuleDocs = false; - } - if (fs.pathExistsSync(path.join(libsDir, name, 'CHANGELOG.md'))) { - this.currentModuleLogs = md.render(fs.readFileSync(path.join(libsDir, name, 'CHANGELOG.md'), { - encoding: 'utf8' - }) || ''); - } else { - this.currentModuleLogs = false; - } - if (fs.pathExistsSync(path.join(libsDir, name, 'LICENSE'))) { - this.currentModuleLicense = fs.readFileSync(path.join(libsDir, name, 'LICENSE'), { - encoding: 'utf8' - }) || ''; - } else { - this.currentModuleLicense = false; - } - this.currentModule.injects = fs.pathExistsSync(path.join(libsDir, name, 'injects')); - this.update(); - }); - this.tab = 'moduleinfo'; - }; - - this.importModules = e => { - const {files} = e.target; - if (files.length === 0) { - return; - } - const value = files[0].path; - e.target.value = ''; - let parentName = null; - let moduleName = null; - const entries = []; - fs.createReadStream(value) - .pipe(unzipper.Parse()) - .on('entry', async entry => { - const fileName = entry.path.toLowerCase(); - if (path.basename(fileName) === 'module.json') { - // consume the entry by buffering the contents into memory. - // eslint-disable-next-line require-atomic-updates - const content = entry.tmpContent = await entry.buffer(); - const json = JSON.parse(content.toString()); - moduleName = json.main.packageName || path.basename(value, '.zip'); - const indexOf = fileName.indexOf('/'); - if (indexOf !== -1) { - parentName = fileName.substring(0, indexOf); - } - } - entries.push(entry); - }) - .on('finish', async () => { - if (moduleName !== null) { - // create a parent directory - await fs.ensureDir(path.join(libsDir, moduleName)); - for (const entry of entries) { - const filePath = entry.path; - const indexOf = filePath.indexOf('/'); - if (filePath === parentName) { - continue; - } - if (indexOf !== -1) { - if (parentName !== null) { - entry.path = `${moduleName}${filePath.substring(indexOf)}`; - } else { - entry.path = `${moduleName}/${filePath}`; - } - } else { - entry.path = `${moduleName}/${filePath}`; - } - entry.path = path.join(libsDir, entry.path); - // 'Directory' or 'File' - if (entry.type === 'Directory') { - // eslint-disable-next-line no-await-in-loop - await fs.ensureDir(entry.path); - } else { - const fileName = entry.path.toLowerCase(); - if (fileName.endsWith('module.json')) { - const content = entry.tmpContent; - // eslint-disable-next-line no-await-in-loop - await fs.writeFile(entry.path, content); - } else { - entry.pipe(fs.createWriteStream(entry.path)) - .on('error', (e) => { - alertify.error(e); - console.error(e); - }); - } - } - } - this.allModules.push(moduleName); - this.update(); - } - }); - }; diff --git a/src/riotTags/notepad-panel.tag b/src/riotTags/notepad-panel.tag index 2ec9cc7c0..3218c8b55 100644 --- a/src/riotTags/notepad-panel.tag +++ b/src/riotTags/notepad-panel.tag @@ -12,6 +12,10 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") svg.feather use(xlink:href="data/icons.svg#life-buoy") span {voc.helppages} + li(onclick="{changeTab('modulespages')}" class="{active: tab === 'modulespages'}") + svg.feather + use(xlink:href="data/icons.svg#ctmod") + span {voc.modulespages} div div(show="{tab === 'notepadlocal'}") .aCodeEditor(ref="notepadlocal") @@ -22,6 +26,8 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") button.aHomeButton(title="{voc.backToHome}" onclick="{backToHome}") svg.feather use(xlink:href="data/icons.svg#home") + div(show="{tab === 'modulespages'}") + docs-panel button.vertical.dockleft(onclick="{notepadToggle}") svg.feather @@ -67,10 +73,6 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") this.getIfDarkTheme = () => localStorage.UItheme === 'Night' || localStorage.UItheme === 'Horizon'; - this.backToHome = () => { - this.refs.helpIframe.contentWindow.location = `http://localhost:${fileServer.address().port}/`; - }; - this.on('update', () => { this.notepadlocal.setValue(global.currentProject.notes || ''); }); @@ -105,10 +107,10 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") cleanUrls: true }; const handler = require('serve-handler'); - fileServer = require('http').createServer((request, response) => { - return handler(request, response, fileServerSettings); - }); + const fileServer = require('http').createServer((request, response) => + handler(request, response, fileServerSettings)); fileServer.listen(0, () => { + // eslint-disable-next-line no-console console.info(`[ct.docs] Running docs server at http://localhost:${fileServer.address().port}`); }); this.server = fileServer; @@ -119,7 +121,12 @@ notepad-panel#notepad.panel.dockright(class="{opened: opened}") this.opened = true; this.update(); }; + + this.backToHome = () => { + this.refs.helpIframe.contentWindow.location = `http://localhost:${fileServer.address().port}/`; + }; + window.signals.on('openDocs', openDocs); this.on('unmount', () => { window.signals.off('openDocs', openDocs); - }); \ No newline at end of file + }); diff --git a/src/riotTags/project-settings/modules/module-meta.tag b/src/riotTags/project-settings/modules/module-meta.tag new file mode 100644 index 000000000..8be63d7d4 --- /dev/null +++ b/src/riotTags/project-settings/modules/module-meta.tag @@ -0,0 +1,119 @@ +module-meta + .flexrow + div + h1 {opts.module.manifest.main.name} + code {opts.module.name} v{opts.module.manifest.main.version} + label.bigpower( + onclick="{toggleModule(opts.module.name)}" + class="{off: !(opts.module.name in global.currentProject.libs)}" + ).nogrow + svg.feather + use(xlink:href="data/icons.svg#{opts.module.name in global.currentProject.libs? 'check' : 'x'}") + span + .hsub(if="{opts.module.manifest.main.tagline}") {opts.module.manifest.main.tagline} + + div(if="{opts.module.manifest.dependencies && opts.module.manifest.dependencies.length}") + b {voc.dependencies} + .inlineblock(each="{dependency in opts.module.manifest.dependencies}") + svg.feather(if="{dependency in global.currentProject.libs}").success + use(xlink:href="data/icons.svg#check") + svg.feather(if="{!(dependency in global.currentProject.libs)}").error + use(xlink:href="data/icons.svg#alert-circle") + span {dependency} + div(if="{opts.module.manifest.optionalDependencies && opts.module.manifest.optionalDependencies.length}") + b {voc.optionalDependencies} + .inlineblock(each="{dependency in opts.module.manifest.optionalDependencies}") + svg.feather(if="{dependency in global.currentProject.libs}").success + use(xlink:href="data/icons.svg#check") + svg.feather(if="{!(dependency in global.currentProject.libs)}").warning + use(xlink:href="data/icons.svg#alert-triangle") + span {dependency} + + .filler + + .flexrow + .aModuleAuthorList + a.external(each="{author in opts.module.manifest.main.authors}" title="{voc.author}" href="{author.site || 'mailto:'+author.mail}") + svg.feather + use(xlink:href="data/icons.svg#user") + span {author.name} + svg.feather.aModuleIcon + use(xlink:href="data/icons.svg#{getIcon(opts.module)}") + script. + this.namespace = 'modules'; + this.mixin(window.riotVoc); + + const {getIcon} = require('./data/node_requires/resources/modules'); + + this.getIcon = getIcon; + + const glob = require('./data/node_requires/glob'); + + const tryLoadTypedefs = () => { + const fs = require('fs-extra'), + path = require('path'); + if (!(this.opts.module.name in global.currentProject.libs)) { + return; + } + const typedefPath = path.join(this.opts.module.path, 'types.d.ts'); + fs.pathExists(typedefPath) + .then(exists => { + const ts = monaco.languages.typescript; + if (!exists) { + // generate dummy typedefs if none were provided by the module + const catmodTypedefs = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${this.opts.module.name}: any; }`; + glob.moduleTypings[this.opts.module.name] = [ + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) + ]; + return; + } + fs.readFile(typedefPath, 'utf8') + .then(catmodTypedefs => { + glob.moduleTypings[this.opts.module.name] = [ + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) + ]; + }); + }); + }; + const removeTypedefs = () => { + if (glob.moduleTypings[this.opts.module.name]) { + for (const lib of glob.moduleTypings[this.opts.module.name]) { + lib.dispose(); + } + } + }; + const addDefaults = () => { + const {name} = this.opts.module; + for (const field of this.opts.module.manifest.fields) { + if (!global.currentProject.libs[name][field.key]) { + if (field.default) { + global.currentProject.libs[name][field.key] = field.default; + } else if (field.type === 'number') { + global.currentProject.libs[name][field.key] = 0; + } else if (field.type === 'checkbox') { + global.currentProject.libs[name][field.key] = false; + } else { + global.currentProject.libs[name][field.key] = ''; + } + } + } + }; + + this.toggleModule = () => e => { + if (global.currentProject.libs[this.opts.module.name]) { + delete global.currentProject.libs[this.opts.module.name]; + removeTypedefs(); + } else { + global.currentProject.libs[this.opts.module.name] = {}; + tryLoadTypedefs(); + // 'Settings' page + if (this.opts.module.manifest.fields) { + addDefaults(); + } + } + window.signals.trigger('modulesChanged'); + glob.modified = true; + e.stopPropagation(); + }; diff --git a/src/riotTags/project-settings/modules/modules-settings.tag b/src/riotTags/project-settings/modules/modules-settings.tag new file mode 100644 index 000000000..13c1b5786 --- /dev/null +++ b/src/riotTags/project-settings/modules/modules-settings.tag @@ -0,0 +1,196 @@ +modules-settings.panel.view + collapsible-section.aModuleFilter(heading="{voc.filter}" defaultstate="closed" storestatekey="modulesFilter") + .flexrow + label.nogrow + b {parent.vocGlob.search} + br + .aSearchWrap.nm + input.inline(type="text" value="{parent.searchValue}" onkeyup="{parent.searchMod}") + svg.feather + use(xlink:href="data/icons.svg#search") + .aModuleFilterColumns + // The "if" clause in the label hides categories with 0 items + label.checkbox( + each="{category in parent.filterCategories}" + if="{parent.categoriesCounter[category]}" + ) + input( + type="checkbox" + checked="{parent.parent.pickedCategories.indexOf(category) !== -1}" + onchange="{parent.parent.toggleCategory(category)}" + ) + svg.feather.accent1 + use(xlink:href="data/icons.svg#{parent.parent.categoryToIconMap[category]}") + span {parent.parent.voc.categories[category]} ({parent.parent.categoriesCounter[category]}) + collapsible-section(preservedom="yes" hlevel="1" heading="{voc.enabledModules}" defaultstate="opened" storestatekey="modulesEnabled") + .aModuleList + .aModuleCard( + each="{module in parent.enabledModules}" + if="{parent.checkVisibility(module)}" + onclick="{parent.parent.openModule(module)}" + ) + module-meta(module="{module}") + collapsible-section(preservedom="yes" hlevel="1" heading="{voc.availableModules}" defaultstate="opened" storestatekey="modulesAvailable") + .aModuleList + .aModuleCard( + each="{module in parent.allModules}" + if="{parent.checkVisibility(module)}" + onclick="{parent.parent.openModule(module)}" + ) + module-meta(module="{module}") + label.file.flexfix-footer.nmb + input( + type="file" + ref="importmodules" + accept=".zip" + onchange="{importModules}" + ) + .button.wide.inline.nml.nmr + svg.feather + use(xlink:href="data/icons.svg#folder") + span {voc.importModules} + module-viewer(if="{openedModule}" module="{openedModule}" onclose="{closeModule}") + script. + this.namespace = 'modules'; + this.mixin(window.riotVoc); + + const {moduleDir, loadModules, categoryToIconMap} = require('./data/node_requires/resources/modules'); + + this.categoriesCounter = {}; + this.filterCategories = Object.keys(categoryToIconMap).sort(); + this.filterCategories.splice(this.filterCategories.indexOf('default'), 1); + this.categoryToIconMap = categoryToIconMap; + + const path = require('path'), + fs = require('fs-extra'), + unzipper = require('unzipper'); + + this.resortEnabledModules = () => { + this.enabledModules = this.allModules + .filter(module => module.name in global.currentProject.libs); + }; + loadModules().then(modules => { + // Sort by their codename (folder name) + this.allModules = modules.sort((a, b) => a.name.localeCompare(b.name)); + this.categoriesCounter = {}; + for (const category of this.filterCategories) { + this.categoriesCounter[category] = 0; + } + for (const module of this.allModules) { + if (module.manifest.main.categories) { + for (const category of module.manifest.main.categories) { + if (this.filterCategories.includes(category)) { + this.categoriesCounter[category]++; + } + } + } + } + this.resortEnabledModules(); + this.update(); + }); + + + this.importModules = e => { + const {files} = e.target; + if (files.length === 0) { + return; + } + const value = files[0].path; + e.target.value = ''; + let parentName = null; + let moduleName = null; + const entries = []; + fs.createReadStream(value) + .pipe(unzipper.Parse()) + .on('entry', async entry => { + const fileName = entry.path.toLowerCase(); + if (path.basename(fileName) === 'module.json') { + // consume the entry by buffering the contents into memory. + // eslint-disable-next-line require-atomic-updates + const content = entry.tmpContent = await entry.buffer(); + const json = JSON.parse(content.toString()); + moduleName = json.main.packageName || path.basename(value, '.zip'); + const indexOf = fileName.indexOf('/'); + if (indexOf !== -1) { + parentName = fileName.substring(0, indexOf); + } + } + entries.push(entry); + }) + .on('finish', async () => { + if (moduleName !== null) { + // create a parent directory + await fs.ensureDir(path.join(moduleDir, moduleName)); + for (const entry of entries) { + const filePath = entry.path; + const indexOf = filePath.indexOf('/'); + if (filePath === parentName) { + continue; + } + if (indexOf !== -1) { + if (parentName !== null) { + entry.path = `${moduleName}${filePath.substring(indexOf)}`; + } else { + entry.path = `${moduleName}/${filePath}`; + } + } else { + entry.path = `${moduleName}/${filePath}`; + } + entry.path = path.join(moduleDir, entry.path); + // 'Directory' or 'File' + if (entry.type === 'Directory') { + // eslint-disable-next-line no-await-in-loop + await fs.ensureDir(entry.path); + } else { + const fileName = entry.path.toLowerCase(); + if (fileName.endsWith('module.json')) { + const content = entry.tmpContent; + // eslint-disable-next-line no-await-in-loop + await fs.writeFile(entry.path, content); + } else { + entry.pipe(fs.createWriteStream(entry.path)) + .on('error', (e) => { + alertify.error(e); + console.error(e); + }); + } + } + } + this.allModules.push(moduleName); + this.update(); + } + }); + }; + + this.searchValue = ''; + this.searchMod = e => { + this.searchValue = e.target.value.trim(); + this.update(); + }; + this.pickedCategories = []; + this.toggleCategory = category => () => { + const ind = this.pickedCategories.indexOf(category); + if (ind !== -1) { + this.pickedCategories.splice(ind, 1); + } else { + this.pickedCategories.push(category); + } + this.update(); + }; + this.checkVisibility = module => { + const visibleDueToSearch = !this.searchValue || + module.name.indexOf(this.searchValue) !== -1 || + module.manifest.main.tagline.indexOf(this.searchValue) !== -1; + const visibleDueToFilters = this.pickedCategories.length === 0 || + module.manifest.main.categories.find(c => this.pickedCategories.indexOf(c) !== -1); + return visibleDueToFilters && visibleDueToSearch; + }; + + this.openModule = module => () => { + this.openedModule = module; + this.update(); + }; + this.closeModule = () => { + this.openedModule = null; + this.update(); + }; diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag index c50e87f73..d501e3f36 100644 --- a/src/riotTags/project-settings/project-settings.tag +++ b/src/riotTags/project-settings/project-settings.tag @@ -1,10 +1,11 @@ project-settings.panel.view.pad.flexrow - - var tabs = ['authoring', 'actions', 'branding', 'scripts', 'rendering']; + var tabs = ['authoring', 'actions', 'branding', 'modules', 'scripts', 'rendering']; var iconMap = { authoring: 'edit', actions: 'airplay', branding: 'droplet', + modules: 'ctmod', rendering: 'room', scripts: 'terminal', default: 'settings' @@ -18,11 +19,24 @@ project-settings.panel.view.pad.flexrow svg.feather use(xlink:href=`data/icons.svg#${(name in iconMap)? iconMap[name] : iconMap.default}`) span='{voc.' + name + '.heading}' + h3(if="{modulesWithDocs.length}") {voc.catmodsSettings} + ul(if="{modulesWithDocs.length}").nav.tabs.vertical + li( + each="{module in modulesWithDocs}" + onclick="{openModuleSettings(module)}" + class="{active: tab === 'moduleSettings' && currentModule === module}" + title="{module.manifest.main.name}" + ) + svg.feather + use(xlink:href=`data/icons.svg#{getIcon(module)}`) + span {module.manifest.main.name} main each name in tabs div(if=`{tab === '${name}'}`) // This outputs a templated tag name. Magic! #{name + '-settings'} + .pad(if="{currentModule}") + extensions-editor(customextends="{currentModule.manifest.fields}" entity="{global.currentProject.libs[currentModule.name]}") script. this.namespace = 'settings'; this.mixin(window.riotVoc); @@ -33,4 +47,26 @@ project-settings.panel.view.pad.flexrow this.tab = 'authoring'; this.openTab = tab => () => { this.tab = tab; - }; \ No newline at end of file + this.currentModule = null; + }; + + const {loadModules, getIcon} = require('./data/node_requires/resources/modules'); + this.getIcon = getIcon; + + this.modulesWithDocs = []; + this.updateModulesWithDocs = async () => { + this.modulesWithDocs = (await loadModules()) + .filter(module => module.name in global.currentProject.libs) + .filter(module => module.manifest.fields); + this.update(); + }; + this.updateModulesWithDocs(); + window.signals.on('modulesChanged', this.updateModulesWithDocs); + this.on('unmount', () => { + window.signals.off('modulesChanged', this.updateModulesWithDocs); + }); + + this.openModuleSettings = module => () => { + this.currentModule = module; + this.tab = 'moduleSettings'; + }; diff --git a/src/riotTags/shared/collapsible-section.tag b/src/riotTags/shared/collapsible-section.tag index 75c4b4815..5d60bc075 100644 --- a/src/riotTags/shared/collapsible-section.tag +++ b/src/riotTags/shared/collapsible-section.tag @@ -3,8 +3,15 @@ @attribute heading (string) The heading to display + @attribute hlevel (integer) + A heading level from 1 to 7. Can be empty; if it is, a regular h3 is shown. @attribute defaultstate ("opened"|"closed") Sets the default state of the section. If it is not set, the section will appear closed. + @attribute storestatekey (string) + If set, remembers the state of this section in localStorage under the specified key. + @attribute preservedom (atomic) + Whether or not to hide the content instead of removing it from DOM. It is recommended + to turn this on for larger layouts. @attribute ontoggle (riot function) A callback that triggers when a user folds/unfolds the section. Passes the new state @@ -12,15 +19,29 @@ collapsible-section .flexrow(onclick="{toggle}") - h3 {opts.heading} + h1(if="{opts.hlevel == 1}") {opts.heading} + h2(if="{opts.hlevel == 2}") {opts.heading} + h3(if="{opts.hlevel == 3 || !opts.hlevel}") {opts.heading} + h4(if="{opts.hlevel == 4}") {opts.heading} + h5(if="{opts.hlevel == 5}") {opts.heading} + h6(if="{opts.hlevel == 6}") {opts.heading} + h7(if="{opts.hlevel == 7}") {opts.heading} svg.feather.a(class="{rotated: this.opened}") use(xlink:href="data/icons.svg#chevron-up") - .collapsible-section-aWrapper(if="{opened}") + .collapsible-section-aWrapper(if="{opened || opts.preservedom}" hide="{!opened && opts.preservedom}") script. - this.opened = this.opts.defaultstate === 'opened'; + if (this.opts.storestatekey) { + this.opened = (localStorage['collapsible-section::' + this.opts.storestatekey] || + this.opts.defaultstate) === 'opened'; + } else { + this.opened = this.opts.defaultstate === 'opened'; + } this.toggle = () => { this.opened = !this.opened; + if (this.opts.storestatekey) { + localStorage['collapsible-section::' + this.opts.storestatekey] = this.opened ? 'opened' : 'closed'; + } if (this.opts.ontoggle) { this.opts.ontoggle(this.opened, this); } diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index c6bce8654..bebe6350f 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -153,7 +153,8 @@ extensions-editor this.on('update', () => { if (!this.opts.entity) { console.error('extension-editor tag did not receive its `entity` object for editing!'); - console.log(this); + // eslint-disable-next-line no-console + console.warn(this); } if (this.opts.customextends && this.opts.customextends !== this.extensions) { this.extensions = this.opts.customextends; diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index d0927590e..9651433a7 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -252,7 +252,7 @@ sounds-panel, rooms-panel border-top 1px solid error transform translate(-0.25rem, 0) rotate(45deg) -.nicetable, #moduleinfo table, #modulesettings table, #modulehelp table +.nicetable margin 1rem 0 border 1px solid borderBright border-radius br diff --git a/src/styl/tags/docs-panel.styl b/src/styl/tags/docs-panel.styl new file mode 100644 index 000000000..561ea92b9 --- /dev/null +++ b/src/styl/tags/docs-panel.styl @@ -0,0 +1,52 @@ +docs-panel + & > .flexrow + height 100% + .&-aNavigation + padding 1rem 0 1rem 1rem + overflow auto + box-sizing border-box + width 20rem + flex 0 0 auto + height 100% + line-height 1.75 + h3 + &:first-child + margin-top 0 + &:not(.a) + color text + .&-Docs + overflow auto + height 100% + padding 1rem 2rem 2rem + box-sizing border-box + + aside + ul + padding 0.25rem 0 0.25rem 1.25rem + margin 0 + list-style none + line-height 1.5 + li.a + margin-top 0.5rem + padding-right 1.5rem + color text + transition 0.35s ease color + &.active + font-weight bold + color accent1 + border-right 2px solid + h3 + margin 1.25rem 0 0 + &.a + transition 0.35s ease color + &.active + font-weight bold + color accent1 + border-right 2px solid + + .&-DocsWidthLimiter + max-width 60rem + margin 0 auto + + table + @extends .nicetable diff --git a/src/styl/tags/modules-panel.styl b/src/styl/tags/modules-panel.styl deleted file mode 100644 index 350d0b294..000000000 --- a/src/styl/tags/modules-panel.styl +++ /dev/null @@ -1,52 +0,0 @@ -modules-panel - display block - .nav - border-bottom-left-radius 0 - border-bottom-right-radius 0 - -#moduleincluded, #modulelist - list-style none - line-height 2.3 - padding-left 0.5em - li - cursor pointer - position relative - left 0 - {trans} - &:hover - left 0.4em - svg - margin-right 0.5em - -#modulelist li - opacity 0.65 - &:hover - opacity 1 -#modname - margin 0 - .version - font-size 0.5em - font-weight 400 - &:before - content "v." - margin-left 1em -#moduleinfo .bigpower - margin-right 1em -#modinjects, #modconfigurable, #modinputmethods - cursor help - margin-left 0.35rem -#modsite svg - margin-right 0.5rem -#modulesettings - textarea - width 100% - box-sizing border-box - height 10em - resize vertical - textarea:active - transition 0 none - .desc p - margin-top 0 -#modinfohtml, #modulehelp, #modulelogs - -webkit-user-select text - cursor text diff --git a/src/styl/tags/settings/modules/module-meta.styl b/src/styl/tags/settings/modules/module-meta.styl new file mode 100644 index 000000000..a8820c0e7 --- /dev/null +++ b/src/styl/tags/settings/modules/module-meta.styl @@ -0,0 +1,33 @@ +module-meta + h1 + margin 0 0 0.35rem + .bigpower + margin-left 1em + .aModuleFeature + cursor help + margin-left 0.35rem + .aModuleCodename + float right + opacity 0.65 + .aModuleAuthorList + margin-top 0.75rem + font-size 90% + a + margin-right 1rem + display inline-block + svg.feather + margin-right 0.35rem + width 1.25rem + height 1.25rem + .aModuleIcon + width 2rem + height 2rem + stroke-width 1.1px + vertical-align top + color accent1 + margin-left 1rem + flex 0 0 auto + align-self flex-end + .hsub + line-height 1.5 + margin 0.5rem 0 \ No newline at end of file diff --git a/src/styl/tags/settings/modules/modules-settings.styl b/src/styl/tags/settings/modules/modules-settings.styl new file mode 100644 index 000000000..74aa9c9a1 --- /dev/null +++ b/src/styl/tags/settings/modules/modules-settings.styl @@ -0,0 +1,40 @@ +.view modules-settings + padding 1em + display block + background background + .desc p + margin-top 0 + .aModuleList + display grid + grid-template-columns repeat(auto-fill, minmax(25em, 1fr)) + grid-gap 1rem + module-meta + height 100% + display flex + flex-flow column nowrap + & > * + flex 0 0 auto + .filler + flex 1 1 auto + .aModuleCard + @extends .panel + padding 1rem 1rem 1rem 1.5rem + cursor pointer + {trans} + {shad} + &:hover + transform translate(0, -0.125rem) + border-color act + {transshort} + {shadamb} + .aModuleFilter + button + margin-top 0.3em + .feather + color act + .aModuleFilterColumns + column-width 14rem + column-gap 1rem + margin-left 2rem + .aSearchWrap + margin 1rem 0 diff --git a/src/styl/tags/settings/project-settings.styl b/src/styl/tags/settings/project-settings.styl index e72062a57..59c4fd70a 100644 --- a/src/styl/tags/settings/project-settings.styl +++ b/src/styl/tags/settings/project-settings.styl @@ -13,8 +13,10 @@ project-settings li padding-top 0.35rem !important padding-bottom @padding-top + h3 + margin 0.75rem 0 0 1rem main flex 1 1 auto padding 0.5rem 1.5rem border-radius 0 br br br - @extends .panel \ No newline at end of file + @extends .panel diff --git a/src/styl/tags/shared/collapsible-section.styl b/src/styl/tags/shared/collapsible-section.styl index 5652ce290..72055a149 100644 --- a/src/styl/tags/shared/collapsible-section.styl +++ b/src/styl/tags/shared/collapsible-section.styl @@ -19,6 +19,7 @@ collapsible-section {trans} margin-left 0.5rem flex 0 0 auto + align-self flex-end &.rotated transform rotate(180deg) .&-aWrapper diff --git a/src/styl/tags/shared/extensions-editor.styl b/src/styl/tags/shared/extensions-editor.styl index f390c8072..f2b5d3ff7 100644 --- a/src/styl/tags/shared/extensions-editor.styl +++ b/src/styl/tags/shared/extensions-editor.styl @@ -2,4 +2,9 @@ extensions-editor type-selector.view, texture-selector.view position fixed z-index 9 - cursor default \ No newline at end of file + cursor default + project-settings & textarea + width 35rem + height 10rem + box-sizing border-box + max-width 100% diff --git a/src/styl/themeDay.styl b/src/styl/themeDay.styl index f019ebd2e..c09f35495 100644 --- a/src/styl/themeDay.styl +++ b/src/styl/themeDay.styl @@ -1,58 +1,58 @@ -@charset "utf-8" - -background = #fff -foreground = #333 -shadows = #000 - -/* Frequently used properties */ -trans = - transition 0.35s ease all -transshort = - transition 0.15s ease all -shad = - box-shadow 0 0.1rem 0.2rem rgba(shadows, 0.5) -shadamb = - box-shadow 0 0 0.35rem rgba(shadows, 0.5) - -/* Base fonts for UI */ -fonts = font = 'Open Sans', sans-serif, serif -font-mono = mono = Iosevka, monospace - -br = 0.2rem -iconsize = 1.5rem - -/* Colors used by this theme */ -act = #446adb -acttext = #446adb -accent1 = #5144db -accent2 = #44dbb5 -error = #d44f57 -red = error -success = #4ab660 -green = success -warning = #ff9748 -orange = warning - -theme = 'Day' -themeDark = false - -borderPale = #e1e2e5 -borderBright = #c8cdd1 - -text = #666 -backgroundDeeper = #fafafa - - -@require 'hvost.styl' - -@require '3rdParty/*.styl' -@require './../../app/node_modules/highlight.js/styles/tomorrow.css' - -@require 'common.styl' -@require 'inputs.styl' -@require 'typography.styl' -@require 'confetti.styl' -@require 'buildingBlocks.styl' -@require 'tabs.styl' - -@require 'tags/**/*.styl' \ No newline at end of file +@charset "utf-8" + +background = #fff +foreground = #333 +shadows = #000 + +/* Frequently used properties */ +trans = + transition 0.35s ease all +transshort = + transition 0.15s ease all +shad = + box-shadow 0 0.1rem 0.1rem rgba(shadows, 0.05), 0 0.2rem 0.3rem rgba(shadows, 0.1), 0 0.3rem 0.5rem rgba(shadows, 0.05) +shadamb = + box-shadow 0 0.1rem 0.25rem rgba(shadows, 0.15), 0 0.5rem 0.5rem rgba(shadows, 0.08), 0 1rem 1rem rgba(shadows, 0.05), 0 1.5rem 1.5rem rgba(shadows, 0.03) + +/* Base fonts for UI */ +fonts = font = 'Open Sans', sans-serif, serif +font-mono = mono = Iosevka, monospace + +br = 0.2rem +iconsize = 1.5rem + +/* Colors used by this theme */ +act = #446adb +acttext = #446adb +accent1 = #5144db +accent2 = #44dbb5 +error = #d44f57 +red = error +success = #4ab660 +green = success +warning = #ff9748 +orange = warning + +theme = 'Day' +themeDark = false + +borderPale = #e1e2e5 +borderBright = #c8cdd1 + +text = #666 +backgroundDeeper = #fafafa + + +@require 'hvost.styl' + +@require '3rdParty/*.styl' +@require './../../app/node_modules/highlight.js/styles/tomorrow.css' + +@require 'common.styl' +@require 'inputs.styl' +@require 'typography.styl' +@require 'confetti.styl' +@require 'buildingBlocks.styl' +@require 'tabs.styl' + +@require 'tags/**/*.styl' diff --git a/src/styl/themeSpringStream.styl b/src/styl/themeSpringStream.styl index ec6b603dd..5f373e3e3 100644 --- a/src/styl/themeSpringStream.styl +++ b/src/styl/themeSpringStream.styl @@ -1,123 +1,123 @@ -@charset "utf-8" - -background = #fff -foreground = #555 -shadows = #000 - -/* Frequently used properties */ -trans = - transition 0.35s ease all -transshort = - transition 0.15s ease all -shad = - box-shadow 0 0.1rem 0.35rem rgba(shadows, 0.25) -shadamb = - box-shadow 0 0.25rem 1rem rgba(shadows, 0.15) - -@font-face - font-family 'Comfortaa' - src url('../data/fonts/Comfortaa-Bold.ttf') - font-weight 700 - font-style normal -@font-face - font-family 'Comfortaa' - src url('../data/fonts/Comfortaa-SemiBold.ttf') - font-weight 600 - font-style normal -@font-face - font-family 'Comfortaa' - src url('../data/fonts/Comfortaa-Medium.ttf') - font-weight 500 - font-style normal -@font-face - font-family 'Comfortaa' - src url('../data/fonts/Comfortaa-Regular.ttf') - font-weight 400 - font-style normal -@font-face - font-family 'Comfortaa' - src url('../data/fonts/Comfortaa-Light.ttf') - font-weight 300 - font-style normal - -/* Base fonts for UI */ -fonts = font = 'Open Sans', sans-serif, serif -fontsHeaders = fontHeader = Comfortaa, 'Open Sans', sans-serif, serif -font-mono = mono = Iosevka, monospace - -br = 0.35rem -iconsize = 1.5rem - -/* Colors used by this theme */ -act = #00c09e -acttext = #009170 -accent1 = #00c09e -accent2 = #008bad -error = #dd3b98 -red = error -success = #009170 -green = success -warning = #ce5a24 -orange = warning - -theme = 'Spring Stream' -themeDark = false - -borderPale = #d6dedd -borderBright = #d6dedd - -text = #555 -backgroundDeeper = #fafafa - -introBg = #f2fcfa - -@require 'hvost.styl' - -@require '3rdParty/*.styl' -@require './../../app/node_modules/highlight.js/styles/tomorrow.css' - -@require 'common.styl' -@require 'inputs.styl' -@require 'typography.styl' -@require 'confetti.styl' -@require 'buildingBlocks.styl' -@require 'tabs.styl' - -@require 'tags/**/*.styl' - -h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, button, .button - font-family fontsHeaders - font-weight 300 - - -h2, .h2 - font-weight 600 - -h3, h4, h5, h6, .h3, .h4, .h5, .h6 - font-weight 900 - -button, -input[type="button"], -input[type="submit"], -input[type="reset"], -.button, -.selectbox - font-weight 900 - border-width 0 - background accent1 - color background - svg - color background - &.selected - background acttext - -#bg - background introBg - -.cards - box-sizing border-box - padding 0.25rem - gap 0.75rem - li - {shad} - border-color background \ No newline at end of file +@charset "utf-8" + +background = #fff +foreground = #555 +shadows = #000 + +/* Frequently used properties */ +trans = + transition 0.35s ease all +transshort = + transition 0.15s ease all +shad = + box-shadow 0 0.1rem 0.35rem rgba(shadows, 0.25) +shadamb = + box-shadow 0 0.125rem 0.3rem rgba(shadows, 0.08), 0 0.25rem 0.7rem rgba(shadows, 0.06), 0 0.5rem 1rem rgba(shadows, 0.04) + +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Bold.ttf') + font-weight 700 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-SemiBold.ttf') + font-weight 600 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Medium.ttf') + font-weight 500 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Regular.ttf') + font-weight 400 + font-style normal +@font-face + font-family 'Comfortaa' + src url('../data/fonts/Comfortaa-Light.ttf') + font-weight 300 + font-style normal + +/* Base fonts for UI */ +fonts = font = 'Open Sans', sans-serif, serif +fontsHeaders = fontHeader = Comfortaa, 'Open Sans', sans-serif, serif +font-mono = mono = Iosevka, monospace + +br = 0.35rem +iconsize = 1.5rem + +/* Colors used by this theme */ +act = #00c09e +acttext = #009170 +accent1 = #00c09e +accent2 = #008bad +error = #dd3b98 +red = error +success = #009170 +green = success +warning = #ce5a24 +orange = warning + +theme = 'Spring Stream' +themeDark = false + +borderPale = #d6dedd +borderBright = #d6dedd + +text = #555 +backgroundDeeper = #fafafa + +introBg = #f2fcfa + +@require 'hvost.styl' + +@require '3rdParty/*.styl' +@require './../../app/node_modules/highlight.js/styles/tomorrow.css' + +@require 'common.styl' +@require 'inputs.styl' +@require 'typography.styl' +@require 'confetti.styl' +@require 'buildingBlocks.styl' +@require 'tabs.styl' + +@require 'tags/**/*.styl' + +h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, button, .button + font-family fontsHeaders + font-weight 300 + + +h2, .h2 + font-weight 600 + +h3, h4, h5, h6, .h3, .h4, .h5, .h6 + font-weight 900 + +button, +input[type="button"], +input[type="submit"], +input[type="reset"], +.button, +.selectbox + font-weight 900 + border-width 0 + background accent1 + color background + svg + color background + &.selected + background acttext + +#bg + background introBg + +.cards + box-sizing border-box + padding 0.25rem + gap 0.75rem + li + {shad} + border-color background diff --git a/src/styl/typography.styl b/src/styl/typography.styl index 15c47c99f..2459888ea 100644 --- a/src/styl/typography.styl +++ b/src/styl/typography.styl @@ -1,52 +1,63 @@ -h1, h2, h3, h4, h5, h6 - margin-bottom 0 - color accent1 -h3, h4, h5, h6, h2 - font-weight 400 -h1 - font-weight 300 -a, .a - color act - cursor pointer - text-decoration none - {trans} - &:hover - color accent1 - &.error - color error - &.success - color success - &.warning - color warning - .error, .success, .warning - &:hover - opacity 0.8 -dd - margin 0 -dt - font-weight 500 - -pre - background backgroundDeeper - border 1px solid borderBright - border-radius br - padding 0.4em 0.8em - font-family font-mono - overflow auto - max-height 30rem -code - font-family font-mono - &.inline, p > &, li > & - padding 1.5px 0.5rem - margin 0 0.25rem - border-radius br - background borderPale - border 1px solid borderBright - text-shadow 0 1px 0 rgba(background, 0.65) - .active & - color act - -.aNotice - line-height 1.5 - opacity 0.8 - margin 0 \ No newline at end of file +h1, h2, h3, h4, h5, h6 + margin-bottom 0 + color accent1 +h2, h3 + font-weight 400 +h4, h5, h6 + font-weight 700 +h1 + font-weight 300 +a, .a + color act + cursor pointer + text-decoration none + {trans} + &:hover + color accent1 + &.error + color error + &.success + color success + &.warning + color warning + .error, .success, .warning + &:hover + opacity 0.8 +dd + margin 0 +dt + font-weight 500 + +pre + background backgroundDeeper + border 1px solid borderBright + border-radius br + padding 0.4em 0.8em + font-family font-mono + overflow auto + max-height 30rem +code + font-family font-mono + &.inline, p > &, li > & + padding 1.5px 0.5rem + margin 0 0.25rem + border-radius br + background borderPale + border 1px solid borderBright + text-shadow 0 1px 0 rgba(background, 0.65) + .active & + color act + +.aNotice + line-height 1.5 + opacity 0.8 + margin 0 + +blockquote + border-left 2px solid accent1 + margin 1rem 0 + padding 1rem 2rem + p:first-child + margin-top 0 + p:last-child + margin-bottom 0 From 2d8157dffced4c1cb04e31a58b48eea279902bc6 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 10:06:30 +1200 Subject: [PATCH 58/86] :zap: Make whole module card clickable --- .../project-settings/modules/module-meta.tag | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/riotTags/project-settings/modules/module-meta.tag b/src/riotTags/project-settings/modules/module-meta.tag index 8be63d7d4..45635ac60 100644 --- a/src/riotTags/project-settings/modules/module-meta.tag +++ b/src/riotTags/project-settings/modules/module-meta.tag @@ -1,12 +1,9 @@ -module-meta +module-meta(onclick="{toggleModule(opts.module.name)}") .flexrow div h1 {opts.module.manifest.main.name} code {opts.module.name} v{opts.module.manifest.main.version} - label.bigpower( - onclick="{toggleModule(opts.module.name)}" - class="{off: !(opts.module.name in global.currentProject.libs)}" - ).nogrow + label.nogrow.bigpower(class="{off: !(opts.module.name in global.currentProject.libs)}") svg.feather use(xlink:href="data/icons.svg#{opts.module.name in global.currentProject.libs? 'check' : 'x'}") span @@ -33,7 +30,12 @@ module-meta .flexrow .aModuleAuthorList - a.external(each="{author in opts.module.manifest.main.authors}" title="{voc.author}" href="{author.site || 'mailto:'+author.mail}") + a.external( + each="{author in opts.module.manifest.main.authors}" + onclick="{stopPropagation}" + title="{voc.author}" + href="{author.site || 'mailto:'+author.mail}" + ) svg.feather use(xlink:href="data/icons.svg#user") span {author.name} @@ -117,3 +119,7 @@ module-meta glob.modified = true; e.stopPropagation(); }; + + this.stopPropagation = e => { + e.stopPropagation(); + }; From acb16de9533a2e740d846b707da781b67b7ec83c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 10:08:17 +1200 Subject: [PATCH 59/86] :zap: Update Spanish translation for ct.IDE. Update by Stuck Up Creations from the Discord server :sparkles: --- app/data/i18n/Spanish.json | 381 ++++++++++++++++++++++++++++--------- 1 file changed, 291 insertions(+), 90 deletions(-) diff --git a/app/data/i18n/Spanish.json b/app/data/i18n/Spanish.json index a94e88839..7fb7aa66c 100644 --- a/app/data/i18n/Spanish.json +++ b/app/data/i18n/Spanish.json @@ -1,7 +1,7 @@ { "me": { "id": "Spa", - "native": "Spanish", + "native": "Español", "eng": "Spanish" }, "common": { @@ -9,10 +9,10 @@ "addtonotes": "Agregar a notas", "apply": "Aplicar", "cancel": "Cancelar", - "cannotBeEmpty": "Esto no puede estar vacío", - "confirmDelete": "¿Seguro que quieres eliminar {0}? No se puede deshacer.", + "cannotBeEmpty": "Esto no debe estar vacío", + "confirmDelete": "¿Seguro que quiere eliminar {0}? Esta acción no se puede revertir.", "copy": "Copiar", - "copyName": "Copia el nombre", + "copyName": "Copiar el nombre", "ctsite": "Página de Inicio de ct.js", "cut": "Cortar", "delete": "Borrar", @@ -21,39 +21,43 @@ "duplicate": "Duplicar", "exit": "Salir", "exitconfirm": "¿Está seguro de que desea salir?
¡Todos los cambios no guardados se perderán!", - "fastimport": "Importación rápida", + "fastimport": "Importar Rápido", "language": "Lenguaje", "translateToYourLanguage": "¡Traduce ct.js a tu idioma!", "name": "Nombre:", - "nametaken": "Este nombre ya ha sido tomado", - "newname": "Nuevo nombre:", + "nametaken": "Este nombre ya está tomado", + "newname": "Nombre Nuevo:", "no": "No", - "none": "None", - "norooms": "You need at least one room to compile your app.", - "notfoundorunknown": "Unknown file. Make sure the file really exists", + "none": "Ninguno", + "norooms": "Se necesita por lo menos un cuarto para compilar la aplicación.", + "notfoundorunknown": "Archivo desconocido. Asegúrece de que el archivo exista.", "ok": "Ok", "open": "Abrir", "openproject": "Abrir Proyecto...", "paste": "Pegar", "reallyexit": "¿Seguro que quieres salir? Todos los cambios no guardados se perderán.", - "rename": "Renombrar", + "rename": "Cambiar Nombre", "save": "Guardar", - "savedcomm": "Su proyecto fue guardado con éxito.", + "savedcomm": "Su proyecto fue guardado exitosamente.", "saveproject": "Guardar proyecto", "sort": "Ordenar:", - "tilelayer": "capa de baldosas", + "tilelayer": "capa de tiles", "wrongFormat": "Formato de archivo incorrecto", "yes": "Si", - "contribute": "", - "zoom": "Zoom:", + "contribute": "Apoya", + "zoom": "Zoom", "zoomIn": "Acercar", - "zoomOut": "Alejar" + "zoomOut": "Alejar", + "clear": "Limpiar", + "loading": "Cargando...", + "selectDialogue": "Seleccionar...", + "select": "Seleccionar" }, "colorPicker": { "current": "Nuevo", "globalPalette": "Paleta Global", "old": "Antigua", - "projectPalette": "Paleta de proyectos" + "projectPalette": "Paleta del proyecto" }, "docsShortcut": { "openDocs": "Abrir la documentación" @@ -65,7 +69,9 @@ "export": "Exportar", "exportPanel": "Exportar el proyecto", "firstrunnotice": "La primera ejecución para cada plataforma será lenta ya que ct.js descargará y guardará las bibliotecas adicionales necesarias para empacar. Tomará algún tiempo, pero las próximas veces serán casi instantáneas.", - "log": "Registro de mensajes" + "log": "Registro de mensajes", + "windowsCrossBuildWarning": "Para crear para Windows desde Linux/MacOS, es necesario tener Wine instalado en el sistema. Las instrucciones de instalación son diferentes para cada plataforma, las puedes buscar en Google :)", + "cannotBuildForMacOnWin": "Desafortunadamente, Windows no es capaz de crear para Mac. Intente utilizando Linux; por ejemplo, en una maquina virtual. ¡Es 100% gratis!" }, "intro": { "loading": "Por favor espere: ¡los gatitos están ganando velocidad con la luz!", @@ -73,28 +79,30 @@ "button": "Crear", "input": "Nombre del proyecto (letras y dígitos)", "text": "Crear nuevo", - "nameerr": "Nombre de proyecto incorrecto" + "nameerr": "Nombre de proyecto incorrecto", + "saveProjectHere": "Guardar proyecto aquí", + "selectProjectFolder": "Seleccionar folder donde guardar su proyecto" }, "recovery": { "message": "

Recuperación

ct.js ha encontrado un archivo de recuperación. Posiblemente, su proyecto no se guardó correctamente o ct.js se cerró en caso de alguna emergencia. Aquí es cuando estos archivos se modificaron por última vez:

Su archivo elegido: {0} {1}
Archivo de recuperación: {2} {3}

¿Qué archivo debe ct? .js abierto?

", "loadTarget": "Archivo de destino", "loadRecovery": "Recuperación", - "newer": "(más nuevo)", - "older": "(más viejo)" + "newer": "(más reciente)", + "older": "(más antigüo)" }, "homepage": "Página principal", "latestVersion": "La versión $1 está disponible", - "forgetProject": "Olvida este proyecto", - "browse": "Vistazo", + "forgetProject": "Olvidar este proyecto", + "browse": "Buscar", "latest": "Últimos proyectos", "twitter": "Canal de Twitter", "discord": "Comunidad de Discord", - "loadingProject": "", - "loadingProjectError": "", - "unableToWriteToFolders": "" + "loadingProject": "Cargando el proyecto...", + "loadingProjectError": "Este proyecto no se puede abrir debido al siguiente error:", + "unableToWriteToFolders": "¡Ct.js no pudo encontrar un lugar apropiado para tus proyectos! Asegúrece de guardar la aplicación de ct.js en un folder donde tenga permisos para modificar." }, "licensepanel": { - "ctjslicense": "" + "ctjslicense": "Licencia Ct.js (MIT)" }, "menu": { "ctIDE": "ct.IDE", @@ -104,28 +112,47 @@ "license": "Licencia", "min": "Modo Ventana", "recentProjects": "Proyectos recientes", - "rooms": "Rooms", + "rooms": "Cuartos", "save": "Guardar proyecto", "startScreen": "Regresar a la pantalla de inicio", - "settings": "Configuraciones", + "settings": "Configuración", "sounds": "Sonidos", "successZipExport": "Exportado exitosamente a {0}.", - "successZipProject": "Se comprimió con éxito el proyecto para {0}.", + "successZipProject": "El proyecto se comprimió con éxito en {0}.", "ui": "UI", "theme": "Tema", - "themeDay": "Light", - "themeNight": "Dark", + "themeDay": "Claro", + "themeNight": "Oscuro", "types": "Tipos", - "zipExport": "Exportar a .zip", - "zipProject": "Paquete de proyecto a .zip", - "codeFontDefault": "", - "codeFontOldSchool": "", - "codeFontSystem": "", - "codeFontCustom": "", - "newFont": "", - "codeFont": "", - "codeLigatures": "", - "codeDense": "" + "zipExport": "Exportar para la web", + "zipProject": "Comprimir proyecto en .zip", + "codeFontDefault": "Predeterminado (Iosevka Light)", + "codeFontOldSchool": "Vieja Escuela", + "codeFontSystem": "Sistema", + "codeFontCustom": "Personalizado...", + "newFont": "Fuente nueva:", + "codeFont": "Fuente para el código", + "codeLigatures": "Ligaduras", + "codeDense": "Diseño Denso", + "launchHotkeys": "(F5; Alt+F5 para ejecutar en el navegador predeterminado)", + "patrons": "Patrons", + "restart": "Reiniciar", + "project": "Proyecto", + "fx": "Effectos", + "themeSpringSream": "Corriente de Primavera", + "themeLucasDracula": "Lucas Dracula", + "openIncludeFolder": "Abrir folder \"incluir\"", + "troubleshooting": "Solución de problemas", + "toggleDevTools": "Activar/Desactivar las herramientas de desarrollador", + "copySystemInfo": "Copiar información del sistema", + "systemInfoWait": "Un momento, Estoy recolectando la información...", + "systemInfoDone": "¡Terminado!", + "disableAcceleration": "Deshabilitar aceleración gráfica (reinicio necesario)", + "disableBuiltInDebugger": "Deshabilitar el depurador integrado", + "visitDiscordForGamedevSupport": "Unete a nuestro servidor de Discord para obtener ayuda", + "postAnIssue": "Reportar un problema en Github...", + "openProject": "Abrir proyecto...", + "openExample": "Abrir un proyecto de ejemplo..." }, "settings": { "actions": { @@ -151,33 +178,55 @@ }, "rendering": { "heading": "Opciones de render", - "framerate": "Cuadros por segundo:", - "pixelatedrender": "Deshabilite el suavizado de imagen aquí y en el proyecto exportado (conserve píxeles nítidos)" + "framerate": "Fotogramas por segundo:", + "pixelatedrender": "Deshabilite el suavizado de imagen aquí y en el proyecto exportado (conserve píxeles nítidos)", + "highDensity": "Soporte de alta densidad de pixeles (e.j. pantallas retina)", + "maxFPS": "Fotogramas máximos:", + "usePixiLegacy": "Añade un renderizador basado en lienzo de legado para el soporte de navegadores antiguos y tarjetas graficas (añade ~20kb al juego)", + "desktopBuilds": "Para escritorio", + "launchMode": "Modo de lanzamiento:", + "launchModes": { + "maximized": "Maximizado", + "fullscreen": "Pantalla completa", + "windowed": "Ventana" + } }, "scripts": { - "heading": "Scripts", - "addNew": "Agregar un nuevo script", - "deleteScript": "Eliminar el script", - "newScriptComment": "Use scripts para definir funciones frecuentes e importar pequeñas bibliotecas", - "moveUp": "Subir", - "moveDown": "Muévete hacia abajo" + "heading": "Scripts personalizados", + "addNew": "Agregar una nueva script", + "deleteScript": "Eliminar la script", + "newScriptComment": "Usa scripts para definir funciones frecuentes e importar pequeñas bibliotecas", + "moveUp": "Mover hacia arriba", + "moveDown": "Mover hacia abajo" + }, + "branding": { + "heading": "Marca", + "accent": "Acento:", + "accentNotice": "Establece el color del precargador, asi como algunos lugares si es usada como aplicación mobil.", + "icon": "Icono del juego:", + "iconNotice": "Deberia ser un cuadro, una textura de un fotograma que sea por lo menos 256x256px de tamaño.", + "invertPreloaderScheme": "Invertir el color del esquema del precargador" } }, "modules": { "author": "Autor de este catmod", "help": "Referencia", - "info": "Info", + "info": "Información", "license": "Licencia", "logs": "Changelog", - "methods": "Metodos", - "parameters": "Parametros", + "methods": "Métodos", + "parameters": "Parámetros", "logs2": "Changelog", - "settings": "Configuraciones" + "settings": "Configuración", + "dependencies": "Dependencias:", + "optionalDependencies": "Dependencias opcionales:", + "importModules": "Módulos a Importar" }, "texture": { "create": "Crear", "import": "Importar", - "skeletons": "Animacion de esqueleto" + "skeletons": "Animacion de esqueleto", + "createType": "Crear tipo de esta" }, "textureview": { "bgcolor": "Cambiar color de fondo", @@ -196,23 +245,25 @@ "rows": "Filas:", "setcenter": "Centro de la imagen", "speed": "Cuadros por segundo:", - "tiled": "¿Está tileado?", + "tiled": "¿Usar como fondo?", "replacetexture": "Reemplazar…", - "corrupted": "¡El archivo está dañado o falta! Cerrando ahora.", + "corrupted": "¡El archivo está dañado o no existe! Cerrando ahora.", "showmask": "Mostrar máscara", "width": "Anchura:", "height": "Altura:", "marginx": "Margen X:", "marginy": "Margen Y:", - "offx": "Offset X:", - "offy": "Offset Y:", - "strip": "Franja de línea / polígono", - "removePoint": "Eliminar el punto", - "closeShape": "Cerrar la forma", - "addPoint": "Agrega un punto", + "offx": "Compensación X:", + "offy": "Compensación Y:", + "strip": "Línea / Polígono", + "removePoint": "Eliminar punto", + "closeShape": "Cerrar forma", + "addPoint": "Agrega punto", "moveCenter": "Mover eje", "movePoint": "Mueve este punto", - "symmetryTool": "Herramienta de simetría" + "symmetryTool": "Herramienta de simetría", + "padding": "Padding:", + "paddingNotice": "Esto afecta la forma en que la textura es exportada: añade pixeles duplicados en las orillas y previene errores de colado en texturas tile y agrandadas. El valor predeterminado usualmente es suficiente, pero, si se encogen las texturas lo suficiente, el colado puede reaparecer. Incrementa este valor si la textura tiene errores durante el juego." }, "sounds": { "create": "Crear" @@ -222,7 +273,7 @@ "name": "Nombre:", "save": "Guardar", "isMusicFile": "Esta es una pista musical", - "poolSize": "Tamaño de la piscina:" + "poolSize": "Tamaño del grupo de sonido (pool):" }, "styles": { "create": "Crear", @@ -243,15 +294,15 @@ "filltype": "Tipo de relleno:", "fillvertical": "Vertical", "font": "Fuente", - "fontweight": "Weight:", + "fontweight": "Peso:", "fontsize": "Tamaño de fuente:", - "fontfamily": "Familia tipográfica:", - "italic": "Italico", + "fontfamily": "Familia de Fuente:", + "italic": "Italica", "lineHeight": "Altura de la línea:", "shadow": "Sombra", - "shadowblur": "Blur:", + "shadowblur": "Difuminado:", "shadowcolor": "Color de la sombra:", - "shadowshift": "Shift:", + "shadowshift": "Cámbio:", "stroke": "Trazo", "strokecolor": "Color del trazo:", "strokeweight": "Grosor de línea:", @@ -262,13 +313,29 @@ "fonts": { "fonts": "Fuentes", "import": "Importar un TTF", - "italic": "Italico" + "italic": "Italica" }, "fontview": { "typefacename": "Nombre de tipo de letra:", "fontweight": "Peso de la fuente:", - "italic": "Es cursiva?", - "reimport": "Reimportar" + "italic": "Es italica?", + "reimport": "Reimportar", + "generateBitmapFont": "Tambien generar fuente bitmap", + "bitmapFont": "Fuente bitmap", + "bitmapFontSize": "Tamaño de fuente:", + "bitmapFontLineHeight": "Altura de Línea", + "resultingBitmapFontName": "Nombre de recurso", + "charset": "Configuración de caracteres:", + "charsets": { + "punctuation": "Digitos y puntuación (usualmente necesario)", + "basicLatin": "Latín Básico", + "latinExtended": "Latín Extendido", + "cyrillic": "Cirílico", + "greekCoptic": "Griego y Copto", + "custom": "Personalizado", + "allInFont": "Dibujar todo lo que la fuente soporta" + }, + "customCharsetHint": "Escriba todas las letras que deseé incluir, en mayúsculas y minúsculas." }, "types": { "create": "Crear" @@ -278,38 +345,38 @@ "create": "Al Crearse", "depth": "Profundidad:", "destroy": "Al destruirse", - "done": "Hecho", + "done": "Hécho", "draw": "Dibujar", - "learnAboutTypes": "Aprenda sobre los tipos de codificación", + "learnAboutTypes": "Aprenda sobre los tipos de código", "name": "Nombre:", - "step": "A cada rato" + "step": "En cada paso" }, "rooms": { "create": "Añadir nuevo", - "makestarting": "Set as the starting room" + "makestarting": "Designar como cuarto inicial" }, "roombackgrounds": { "add": "Agregar un fondo", "depth": "Profundidad:", "movement": "Velocidad de movimiento (X, Y):", - "parallax": "Parallax (X, Y):", + "parallax": "Paralaje (X, Y):", "repeat": "Repetir:", - "scale": "Escalado (X, Y):", - "shift": "Shift (X, Y):" + "scale": "Escala (X, Y):", + "shift": "Cambio (X, Y):" }, "roomtiles": { "moveTileLayer": "Mover a una nueva profundidad", "show": "Mostrar la capa", "hide": "Ocultar la capa", - "findTileset": "Encuentra un tileset" + "findTileset": "Encontrar un tileset" }, "roomview": { "name": "Nombre:", "width": "Ancho de vista:", - "height": "Altura de la vista:", - "events": "Eventos del Room", + "height": "Altura de vista:", + "events": "Eventos del cuarto", "copies": "Copias", - "backgrounds": "Backgrounds", + "backgrounds": "Fondos", "tiles": "Tiles", "add": "Añadir", "none": "Nada", @@ -323,14 +390,14 @@ "selectbg": "Seleccionar tileset", "shift": "Cambiar la vista", "shifttext": "Cambiar por:", - "step": "A cada rato", - "create": "Al crearcse", + "step": "En cada paso", + "create": "Al crearce", "leave": "Al salir", "draw": "Dibujar", "newdepth": "Nueva profundidad:", "deletecopy": "Eliminar copia {0}", "deleteCopies": "Eliminar copias", - "shiftCopies": "Shift copies", + "shiftCopies": "Cambiar copias", "selectAndMove": "Seleccionar y mover", "changecopyscale": "Cambiar escala", "shiftcopy": "Establecer coordenadas", @@ -338,12 +405,146 @@ "deletetiles": "Eliminar tiles", "movetilestolayer": "Mover a capa", "shifttiles": "Cambiar tiles", - "findTileset": "" + "findTileset": "Encontrar tileset", + "changecopyrotation": "Rotar" }, "notepad": { "local": "Bloc de notas del proyecto", "global": "Bloc de notas global", "helppages": "Ayuda", - "backToHome": "Volver a la página de inicio de los documentos" + "backToHome": "Volver a la página de inicio" + }, + "curveEditor": { + "curveLineHint": "Puede hacer click en la curva para añadir un punto", + "dragPointHint": "Arrastre para mover un punto, click derecho para eliminarlo", + "pointTime": "Tiempo:", + "pointValue": "Valor:" + }, + "debuggerToolbar": { + "pause": "Pausar", + "resume": "Reanudar juego", + "restartGame": "Reiniciar juego", + "restartRoom": "Reiniciar cuarto", + "switchRoom": "Ir al cuarto...", + "toggleDevTools": "Activar/Desactivar herramients de desarrollo", + "screenshot": "Tomar captura de pantalla", + "enterFullscreen": "Ingresar a pantalla completa", + "exitFullscreen": "Salir de pantalla completa", + "links": "Enlaces y codigos QR", + "openExternal": "Abrir en navegador", + "close": "Cerrar" + }, + "onboarding": { + "hoorayHeader": "¡Wow! !Acabas de crear un proyecto!", + "nowWhatParagraph": "Ahora ¿Que deberiamos hacer?", + "openSpaceShooterTutorial": "Aprender como hacer un space shooter", + "openPlatformerTutorial": "Aprender como hacer un juego de plataforma", + "openJettyCatTutorial": "Aprender como hacer un juego de Jetty Cat", + "doNothing": "¡Brincate esta página y crea un juego grandiaso!", + "showOnboardingCheckbox": "Mostrar esta página al crear un proyecto nuevo" + }, + "particleEmitters": { + "emittersHeading": "Emisores de partículas", + "emitterHeading": "Emisor", + "from": "de:", + "to": "a:", + "textureHeading": "Textura", + "selectTexture": "Seleccionar...", + "importBuiltin": "Importar predeterminado...", + "colorAndOpacityHeading": "Color y Opacidad", + "stepped": "Escalonado", + "steppedColor": "Color de escalonado", + "steppedAlpha": "Opacidad de escalonado", + "blendMode": "Modo de combinado", + "regular": "Regular", + "darken": "Oscurecer", + "lighten": "Aclarar", + "burn": "Quemar", + "scalingHeading": "Escaleado", + "scale": "Escala:", + "minimumSize": "Tamaño minimo", + "minimumSizeHint": "Valores menores harán el tamaño de cada partícula aleatorio. Mientras mas bajo el valor, mas fuerte el efecto.", + "velocityHeading": "Velocidad", + "velocity": "Velocidad:", + "minimumSpeed": "Velocidad minima", + "minimumSpeedHint": "Valores menores harán la velocidad de cada partícula aleatorio. Mientras mas bajo el valor, será más lenta la partícula.", + "maxSpeed": "Velocidad máxima:", + "gravityHeading": "Gravedad", + "gravityNotice": "Note que si difiere de (0,0), la interpolacion de velocidad será deshabilitada, esto quiere decir que solamente el primer nodo de la gráfica de velocidad tendrá el efecto.", + "directionHeading": "Dirección", + "startingDirection": "Dirección Inicial", + "preserveTextureDirection": "Conservar rotación de textura", + "rotationHeading": "Rotación", + "rotationSpeed": "Velocidad de rotación", + "rotationAcceleration": "Aceleración de rotación", + "spawningHeading": "Generación", + "timeBetweenBursts": "Tiempo entre ráfagas:", + "spawnAtOnce": "Generar al instante", + "chanceToSpawn": "Probabilidad para generar una partícula", + "maxParticles": "Máximo de partículas:", + "lifetime": "Tiempo de vida de la partícula, seg.", + "emitterLifetime": "Tiempo de vida del emisor, seg.:", + "prewarmDelay": "Precalentamiento / demora, seg.:", + "prewarmDelayNotice": "Valores negativos preparan la partícula antes de mostrarla, lo que es útil para efectos largos como partículas de polvo o brisa que deverian ser visibles desde el inicio del cuarto; Valores positivos demoraran la simulacion.", + "shapeAndPositioningHeading": "Forma y Posicionamiento", + "spawnType": "Tipo de forma:", + "spawnShapes": { + "point": "Punto", + "rectangle": "Rectangulo", + "circle": "Circulo", + "ring": "Anillo", + "star": "Estrella" + }, + "width": "Anchura:", + "height": "Altura:", + "radius": "Radio:", + "starPoints": "Puntos:", + "startAngle": "Rotación, grad.:", + "showShapeVisualizer": "Mostrar visualizador de forma", + "relativeEmitterPosition": "Posición relativa a otros emisores", + "addEmitter": "Añadir otro emisor", + "reset": "Reestablecer vista previa", + "changeBg": "Cambiar fondo", + "inspectorComplete": "¡Terminado!", + "alreadyHasAnImportingTexture": "Ya existe una textura de nombre $1. Debe eliminarla o cambiarle el nombre; aunque es posible que este importando una textura que ya ah sido importada antes :)", + "changeGrid": "Establecer el tamaño de la cuadricula", + "newGridSize": "Nuevo tamaño de cuadricula:", + "setPreviewTexture": "Establecer vista previa de textura" + }, + "patreon": { + "aboutPatrons": "Los Patrons son personas que muestran su apoyo a ComigoGames en Patreon, en forma de donaciones recurrentes. No todos los Patrons provienen de Ct.js; algunos son usuarios de otras aplicaciones de ComigoGames. Tip: Si eres un creador y donador atravez de Patron, obtendrás un enlace a tu página aquí — Esa es mi pequeña ayuda a tus creaciones :)", + "patronsHeader": "Nuestros patrons", + "businessShuttles": "Shuttles de negocios", + "noShuttlesYet": "Aun no tenemos shuttles de negocios :c ¡Aunque la suya podria ser la primera!", + "shuttlesDescription": "Shuttles de negocios son considerados socios de ct.js. Son listados en la página de inicio de ct.js y sus páginas de distribución.", + "spacePirates": "Piratas Espaciales", + "noPiratesYet": "Aun no tenemos Piratas Espaciales :c", + "piratesDescription": "Los Piratas Espaciales obtienen apoyo prioritario en el servidor de Discord con un rol interesante, y tambien son listados aquí. ", + "spaceProgrammers": "Programadores Espaciales", + "programmersDescription": "\"Programadores Espaciales\" es un tier de legado que estaba disponible anted de que ct.js fuera codigo abierto y contribuia con fuentes de juegos de jams a los patrons.", + "aspiringAstronauts": "Aspirantes de Astronauta", + "noAstronautsYet": "Aun no hay Aspirantes de Astronauta", + "astronautsDescription": "Astronautas obtienen un rol especial en Discord y son listados aquí.", + "thankAllPatrons": "¡Gracias a todos los patrons de ComigoGames, actuales y pasados, su apoyo mantiene Comigo en movimiento hacia adelante y ayuda a crear mejores aplicaciones! :)", + "becomeAPatron": "Convierte en un patron", + "aboutFillers": [ + "es buena onda en general 😎", + "tiene buena conversación 🤗", + "aun tiene que convertirse en estrella 💫", + "simplemente un prodigio ⭐️", + "es un buen amigo 🤝", + "es confiable 🙏", + "tiene un corazón de oro 🧡", + "es un mago 🔮", + "¡esta aquí para ayudar! 💪", + "es un super heróe 🦸‍", + "un tiene que esneñar 🦹‍", + "es un misterio sin resolver 🔍", + "¡es epico! ✨", + "es probablemente un robot 🤖", + "¡es como un fuego ardiente! 🔥", + "trae luz y esperanza 🌞", + "es elegante y hermos@ 🎩" + ] } -} +} \ No newline at end of file From db4409a1ef2ab084ff123d2133f49874a9be90de Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 11:01:24 +1200 Subject: [PATCH 60/86] :bug: Fix missing typedefs at startup, and move typedef loading/unloading logic into node_requires/resources/modules --- src/js/loadProject.js | 4 ++ .../resources/modules/typedefs.js | 59 +++++++++++++++++++ .../project-settings/modules/module-meta.tag | 32 ++-------- .../project-settings/script-editor.tag | 2 +- 4 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 src/node_requires/resources/modules/typedefs.js diff --git a/src/js/loadProject.js b/src/js/loadProject.js index ea52921f8..3f359e3de 100644 --- a/src/js/loadProject.js +++ b/src/js/loadProject.js @@ -126,6 +126,10 @@ document.title = global.currentProject.settings.title + ' — ct.js'; } + const {loadAllTypedefs, resetTypedefs} = require('./data/node_requires/resources/modules/typedefs'); + resetTypedefs(); + loadAllTypedefs(); + setTimeout(() => { window.riot.update(); }, 0); diff --git a/src/node_requires/resources/modules/typedefs.js b/src/node_requires/resources/modules/typedefs.js new file mode 100644 index 000000000..d78051479 --- /dev/null +++ b/src/node_requires/resources/modules/typedefs.js @@ -0,0 +1,59 @@ +const loadedTypings = {}; + +const addTypedefs = async function addTypedefs(module) { + const fs = require('fs-extra'), + path = require('path'); + const typedefPath = path.join(module.path, 'types.d.ts'); + const ts = monaco.languages.typescript; + if (await fs.pathExists(typedefPath)) { + fs.readFile(typedefPath, 'utf8') + .then(catmodTypedefs => { + loadedTypings[module.name] = [ + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) + ]; + }); + } else { + // generate dummy typedefs if none were provided by the module + const catmodTypedefs = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${module.name}: any; }`; + loadedTypings[module.name] = [ + ts.javascriptDefaults.addExtraLib(catmodTypedefs), + ts.typescriptDefaults.addExtraLib(catmodTypedefs) + ]; + } +}; + +const removeTypedefs = function removeTypedefs(module) { + if (loadedTypings[module.name]) { + for (const lib of loadedTypings[module.name]) { + lib.dispose(); + } + } + delete loadedTypings[module.name]; +}; + +const loadAllTypedefs = async function loadAllTypedefs() { + const {loadModules} = require('.'); + for (const module of await loadModules()) { + if (!(module.name in global.currentProject.libs)) { + continue; + } + addTypedefs(module); + } +}; + +const resetTypedefs = function () { + for (const i in loadedTypings) { + for (const lib of loadedTypings[i]) { + lib.dispose(); + } + delete loadedTypings[i]; + } +}; + +module.exports = { + addTypedefs, + removeTypedefs, + loadAllTypedefs, + resetTypedefs +}; diff --git a/src/riotTags/project-settings/modules/module-meta.tag b/src/riotTags/project-settings/modules/module-meta.tag index 45635ac60..b96a68c59 100644 --- a/src/riotTags/project-settings/modules/module-meta.tag +++ b/src/riotTags/project-settings/modules/module-meta.tag @@ -52,39 +52,15 @@ module-meta(onclick="{toggleModule(opts.module.name)}") const glob = require('./data/node_requires/glob'); const tryLoadTypedefs = () => { - const fs = require('fs-extra'), - path = require('path'); if (!(this.opts.module.name in global.currentProject.libs)) { return; } - const typedefPath = path.join(this.opts.module.path, 'types.d.ts'); - fs.pathExists(typedefPath) - .then(exists => { - const ts = monaco.languages.typescript; - if (!exists) { - // generate dummy typedefs if none were provided by the module - const catmodTypedefs = `declare namespace ct {\n/** Sorry, no in-code docs for this module :c */\n var ${this.opts.module.name}: any; }`; - glob.moduleTypings[this.opts.module.name] = [ - ts.javascriptDefaults.addExtraLib(catmodTypedefs), - ts.typescriptDefaults.addExtraLib(catmodTypedefs) - ]; - return; - } - fs.readFile(typedefPath, 'utf8') - .then(catmodTypedefs => { - glob.moduleTypings[this.opts.module.name] = [ - ts.javascriptDefaults.addExtraLib(catmodTypedefs), - ts.typescriptDefaults.addExtraLib(catmodTypedefs) - ]; - }); - }); + const {addTypedefs} = require('./data/node_requires/resources/modules/typedefs'); + addTypedefs(this.opts.module); }; const removeTypedefs = () => { - if (glob.moduleTypings[this.opts.module.name]) { - for (const lib of glob.moduleTypings[this.opts.module.name]) { - lib.dispose(); - } - } + const {removeTypedefs} = require('./data/node_requires/resources/modules/typedefs'); + removeTypedefs(this.opts.module); }; const addDefaults = () => { const {name} = this.opts.module; diff --git a/src/riotTags/project-settings/script-editor.tag b/src/riotTags/project-settings/script-editor.tag index 0ed199bf3..947838472 100644 --- a/src/riotTags/project-settings/script-editor.tag +++ b/src/riotTags/project-settings/script-editor.tag @@ -1,4 +1,4 @@ -script-editor +script-editor.view .flexfix.tall div.flexfix-header b {voc.name} From d09ee4892f3cb04a2817d7b47707f98fce58d82f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 11:01:39 +1200 Subject: [PATCH 61/86] :bug: Fix wrong default setting for ct.fittoscreen --- app/data/ct.libs/fittoscreen/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/ct.libs/fittoscreen/module.json b/app/data/ct.libs/fittoscreen/module.json index 46210ec7f..2475e427e 100644 --- a/app/data/ct.libs/fittoscreen/module.json +++ b/app/data/ct.libs/fittoscreen/module.json @@ -33,7 +33,7 @@ }], "key": "mode", "id": "mode", - "default": "scaleCover", + "default": "scaleFit", "type": "radio" }] } From b171dcb01f83785bbf19f53f00fe2177d3262768 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 11:29:46 +1200 Subject: [PATCH 62/86] :bug: Fix the first tile layer not being added into a drawing stack at room-editor, which made tiles invisible unless a copy or background was added Closes #206 --- src/riotTags/rooms/room-editor.tag | 2 +- src/riotTags/rooms/room-tile-editor.tag | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/riotTags/rooms/room-editor.tag b/src/riotTags/rooms/room-editor.tag index bd9a9d5c5..e6c361904 100644 --- a/src/riotTags/rooms/room-editor.tag +++ b/src/riotTags/rooms/room-editor.tag @@ -497,7 +497,7 @@ room-editor.panel.view ); } } - } else if (this.stack[i].texture) { // это слой-фон + } else if (this.stack[i].texture) { // a background layer if (this.stack[i].texture !== -1) { if (!('extends' in this.stack[i])) { this.stack[i].extends = {}; diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index 6981cee1f..c146107cb 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -41,6 +41,7 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix tiles: [], extends: {} }]; + this.parent.resortRoom(); } [this.parent.currentTileLayer] = this.opts.room.tiles; this.parent.currentTileLayerId = 0; From 86514f8416c1617f6d3882bf954761605e9fe968 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 12:35:41 +1200 Subject: [PATCH 63/86] :bug: Fix broken checkboxes at "export for desktop" panel --- src/riotTags/export-panel.tag | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/riotTags/export-panel.tag b/src/riotTags/export-panel.tag index 32cb5e88c..3a3eea072 100644 --- a/src/riotTags/export-panel.tag +++ b/src/riotTags/export-panel.tag @@ -7,17 +7,17 @@ export-panel p {voc.firstrunnotice} fieldset label.checkbox - input(type="checkbox" checked="{projSettings.export.linux}" onchange="{wire('projSettings.export.linux')}") + input(type="checkbox" checked="{projSettings.export.linux}" onchange="{wire('this.projSettings.export.linux')}") svg.icon use(xlink:href="data/icons.svg#linux") | Linux label.checkbox(disabled="{process.platform === 'win32'}" title="{process.platform === 'win32' && voc.cannotBuildForMacOnWin}") - input(type="checkbox" checked="{projSettings.export.mac}" onchange="{wire('projSettings.export.mac')}") + input(type="checkbox" checked="{projSettings.export.mac}" onchange="{wire('this.projSettings.export.mac')}") svg.icon use(xlink:href="data/icons.svg#apple") | MacOS label.checkbox - input(type="checkbox" checked="{projSettings.export.windows}" onchange="{wire('projSettings.export.windows')}") + input(type="checkbox" checked="{projSettings.export.windows}" onchange="{wire('this.projSettings.export.windows')}") svg.icon use(xlink:href="data/icons.svg#windows") | Windows From 8c9db706a80e2855cfbdf094aac312a7840801e7 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 13:37:11 +1200 Subject: [PATCH 64/86] :zap: Allow games enter fullscreen while being in debugger See #155 --- src/examples/catformer.ict | 60 ++++++++++--------- .../debugger/debugger-screen-embedded.tag | 5 ++ 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/examples/catformer.ict b/src/examples/catformer.ict index 38fdf188d..c8fa287ea 100644 --- a/src/examples/catformer.ict +++ b/src/examples/catformer.ict @@ -285,21 +285,17 @@ types: depth: 100 oncreate: '' onstep: |- - if (ct.mouse.pressed) { - if (ct.u.prect(ct.mouse.x, ct.mouse.y, this)) { - ct.fittoscreen.toggleFullscreen(); - } + if (ct.mouse.pressed && ct.mouse.hoversUi(this)) { + ct.fittoscreen.toggleFullscreen(); } - if (ct.touch.enabled) { - if (ct.u.prect(ct.touch.x, ct.touch.y, this)) { - ct.fittoscreen.toggleFullscreen(); - } + if (ct.touch.enabled && ct.touch.hoversUi(this)) { + ct.fittoscreen.toggleFullscreen(); } ondraw: 'this.gotoAndStop(ct.fittoscreen.getIsFullscreen()? 1 : 0);' ondestroy: '' uid: 8a4b8de4-7729-4936-bc7b-bc277a512975 - lastmod: 1586186372422 + lastmod: 1596761320719 extends: {} texture: d161a39a-203e-4099-99e2-41385d94f975 - name: CoinWidget @@ -387,7 +383,7 @@ types: uid: 86f037f0-9a1c-4306-9d67-00be01bbd793 texture: b5121b5f-8109-45c0-9fa4-44f9f10ce88c extends: {} - lastmod: 1586656259157 + lastmod: 1596761326225 - name: TryAgain depth: 0 oncreate: '' @@ -495,7 +491,7 @@ rooms: texture: fd9d7012-6826-4368-8fe5-0385b72bc902 uid: b0f0ff4d-a155-4d68-a86e-5bb7ed8973ab grid: 35 - lastmod: 1586161145705 + lastmod: 1596761297056 thumbnail: 1 tiles: - depth: -10 @@ -1340,6 +1336,7 @@ rooms: - 1 - 1 texture: 9968de05-f39d-4455-aaed-175c03728bb1 + extends: {} - depth: -20 tiles: - x: 70 @@ -1470,6 +1467,7 @@ rooms: - 1 - 1 texture: 9968de05-f39d-4455-aaed-175c03728bb1 + extends: {} gridX: 35 gridY: 35 copies: @@ -1679,6 +1677,7 @@ rooms: tiles: - depth: -10 tiles: [] + extends: {} copies: - x: 512 'y': 256 @@ -1738,6 +1737,7 @@ rooms: tiles: - depth: -10 tiles: [] + extends: {} copies: - x: 512 'y': 256 @@ -1802,39 +1802,41 @@ rooms: tiles: - depth: -10 tiles: [] + extends: {} uid: cc861d50-35d6-4687-b789-5768eedfd438 thumbnail: 5768eedfd438 gridX: 32 gridY: 64 - lastmod: 1586161332969 + lastmod: 1596761311993 soundtick: 6 roomtick: 3 typetick: 12 styletick: 4 starting: 0 settings: - minifyhtmlcss: true - minifyjs: false - title: Catformer - author: Cosmo Myzrail Gorynych - site: 'https://ctjs.rocks' - fps: 30 - version: - - 0 - - 0 - - 0 - versionPostfix: '' export: linux: true - windows: true - mac: true - maxFPS: 60 - highDensity: true + windows: false + mac: false branding: icon: -1 accent: '#446adb' invertPreloaderScheme: true - desktopMode: maximized + rendering: + maxFPS: 60 + pixelatedrender: false + highDensity: true + usePixiLegacy: true + desktopMode: maximized + authoring: + title: Catformer + author: Cosmo Myzrail Gorynych + site: 'https://ctjs.rocks' + version: + - 0 + - 0 + - 0 + versionPostfix: '' palette: - '#257073' startroom: b0f0ff4d-a155-4d68-a86e-5bb7ed8973ab @@ -1873,7 +1875,7 @@ scripts: depth: 1000 }); } -ctjsVersion: 1.3.0 +ctjsVersion: 1.3.2 fonts: [] skeletons: [] actions: diff --git a/src/riotTags/debugger/debugger-screen-embedded.tag b/src/riotTags/debugger/debugger-screen-embedded.tag index 675a3616d..ab61e4bca 100644 --- a/src/riotTags/debugger/debugger-screen-embedded.tag +++ b/src/riotTags/debugger/debugger-screen-embedded.tag @@ -115,6 +115,11 @@ debugger-screen-embedded(class="{opts.class} {flexrow: verticalLayout, flexcol: /* Bootstrap preview and debug views */ this.on('mount', () => { + this.refs.gameView.addEventListener('permissionrequest', function(e) { + if (e.permission === 'fullscreen') { + e.request.allow(); + } + }); this.refs.gameView.addEventListener('contentload', () => { this.refs.devtoolsView.addEventListener('contentload', () => { this.refs.devtoolsView.executeScript({ From a9ad8943a2cb6f3a7d8eed52e9ca23751c073354 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 13:37:41 +1200 Subject: [PATCH 65/86] :zap: Allow fittoscreen to toggle fullscreen mode while being in an electron app Closes #155 --- app/data/ct.libs/fittoscreen/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/data/ct.libs/fittoscreen/index.js b/app/data/ct.libs/fittoscreen/index.js index 758a6ed01..4728b3649 100644 --- a/app/data/ct.libs/fittoscreen/index.js +++ b/app/data/ct.libs/fittoscreen/index.js @@ -61,6 +61,14 @@ } }; var toggleFullscreen = function () { + try { + // Are we in Electron? + const win = require('electron').remote.BrowserWindow.getFocusedWindow(); + win.setFullScreen(!win.isFullScreen()); + return; + } catch (e) { + void e; // Continue with web approach + } var canvas = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || @@ -113,6 +121,13 @@ }); ct.fittoscreen.mode = $mode; ct.fittoscreen.getIsFullscreen = function getIsFullscreen() { + try { + // Are we in Electron? + const win = require('electron').remote.BrowserWindow.getFocusedWindow; + return win.isFullScreen; + } catch (e) { + void e; // Continue with web approach + } return document.fullscreen || document.webkitIsFullScreen || document.mozFullScreen; }; })(ct); From 80b617493c4389fb2c6c1672ed281c4a4c0d5cf5 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 15:49:35 +1200 Subject: [PATCH 66/86] :sparkles: Custom font selector in the style editor --- app/data/i18n/English.json | 3 +- src/icons/font.svg | 1 + src/riotTags/shared/font-selector.tag | 28 +++++++++++++++++ src/riotTags/style-editor.tag | 31 ++++++++++++++++--- src/styl/tags/fonts-panel.styl | 7 +---- ...re-selector.styl => _asset-selectors.styl} | 2 +- src/styl/tags/shared/asset-viewer.styl | 8 ++++- src/styl/tags/shared/type-selector.styl | 2 -- 8 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 src/icons/font.svg create mode 100644 src/riotTags/shared/font-selector.tag rename src/styl/tags/shared/{texture-selector.styl => _asset-selectors.styl} (55%) delete mode 100644 src/styl/tags/shared/type-selector.styl diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 1753fef5e..0c743c1d8 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -364,7 +364,8 @@ "strokeweight": "Line weight:", "testtext": "Test text 0123 +", "textWrap": "Word wrap", - "textWrapWidth": "Max width:" + "textWrapWidth": "Max width:", + "useCustomFont": "Use custom font…" }, "fonts": { "fonts": "Fonts", diff --git a/src/icons/font.svg b/src/icons/font.svg new file mode 100644 index 000000000..c6b2de334 --- /dev/null +++ b/src/icons/font.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/riotTags/shared/font-selector.tag b/src/riotTags/shared/font-selector.tag new file mode 100644 index 000000000..2ee2b4ec3 --- /dev/null +++ b/src/riotTags/shared/font-selector.tag @@ -0,0 +1,28 @@ +// + Allows users to pick a custom font. + + @attribute header (any string or empty) + An optional header shown in the top-left corner + @attribute onselected (riot function) + A two-fold function (font => e => {…}). Calls the funtion with the selected + font as the only argument in the first function, and MouseEvent in the second. + @attribute oncancelled (riot function) + Calls the funtion when a user presses the "Cancel" button. Passes no arguments. + +font-selector.panel.view.flexfix + .flexfix-body + asset-viewer( + collection="{global.currentProject.fonts}" + namespace="fonts" + click="{opts.onselected}" + thumbnails="{thumbnails}" + names="{names}" + ref="fonts" + class="tall" + ) + h1(if="{opts.header}") {opts.header} + .flexfix-footer(if="{opts.oncancelled}") + button(onclick="{opts.oncancelled}") {window.languageJSON.common.cancel} + script. + this.thumbnails = font => `file://${window.global.projdir}/fonts/${font.origname}_prev.png?cache=${font.lastmod}`; + this.names = font => `${font.typefaceName} ${font.weight} ${font.italic ? this.voc.italic : ''}`; diff --git a/src/riotTags/style-editor.tag b/src/riotTags/style-editor.tag index c71c0d13c..919b7f830 100644 --- a/src/riotTags/style-editor.tag +++ b/src/riotTags/style-editor.tag @@ -15,13 +15,19 @@ style-editor.panel.view #stylefont.tabbed(show="{tab === 'stylefont'}") #stylefontinner fieldset - b {voc.fontfamily} - input#fontfamily.wide(type="text" value="{styleobj.font.family || 'sans-serif'}" onchange="{wire('this.styleobj.font.family')}") - .fifty.npl.npt + label + b {voc.fontfamily} + input#fontfamily.wide(type="text" value="{styleobj.font.family || 'sans-serif'}" onchange="{wire('this.styleobj.font.family')}") + button(onclick="{openCustomFontSelector}") + svg.feather + use(xlink:href="data/icons.svg#font") + span {voc.useCustomFont} + .clear + label.fifty.npl.nmt b {voc.fontsize} br input#fontsize.wide(type="number" value="{styleobj.font.size || '12'}" onchange="{wire('this.styleobj.font.size')}" oninput="{wire('this.styleobj.font.size')}" step="1") - .fifty.npr.npt + label.fifty.npr.nmt b {voc.fontweight} br select.wide(value="{styleobj.font.weight}" onchange="{wire('this.styleobj.font.weight')}") @@ -125,7 +131,7 @@ style-editor.panel.view use(xlink:href="data/icons.svg#check") span {voc.apply} #stylepreview.tall(ref="canvasSlot") - texture-selector(if="{selectingTexture}" onselected="{applyTexture}" ref="textureselector") + font-selector(if="{selectingFont}" onselected="{applyFont}" oncancelled="{cancelCustomFontSelector}") script. const fs = require('fs-extra'); @@ -190,6 +196,21 @@ style-editor.panel.view this.selectingTexture = false; + this.openCustomFontSelector = () => { + this.selectingFont = true; + }; + this.cancelCustomFontSelector = () => { + this.selectingFont = false; + this.update(); + }; + this.applyFont = font => () => { + this.selectingFont = false; + this.styleobj.font.family = `"CTPROJFONT${font.typefaceName}", "${font.typefaceName}", sans-serif`; + this.styleobj.font.weight = font.weight; + this.styleobj.font.italic = font.italic; + this.update(); + }; + this.styleSetAlign = align => () => { this.styleobj.font.halign = align; }; diff --git a/src/styl/tags/fonts-panel.styl b/src/styl/tags/fonts-panel.styl index 135e0206a..ea9cda016 100644 --- a/src/styl/tags/fonts-panel.styl +++ b/src/styl/tags/fonts-panel.styl @@ -1,9 +1,4 @@ fonts-panel padding 1em h1 - margin-bottom 1rem - if (themeDark) - .cards li img - filter invert(1) - &:hover - background transparent + margin-bottom 1rem \ No newline at end of file diff --git a/src/styl/tags/shared/texture-selector.styl b/src/styl/tags/shared/_asset-selectors.styl similarity index 55% rename from src/styl/tags/shared/texture-selector.styl rename to src/styl/tags/shared/_asset-selectors.styl index 26e73fb46..49173671d 100644 --- a/src/styl/tags/shared/texture-selector.styl +++ b/src/styl/tags/shared/_asset-selectors.styl @@ -1,4 +1,4 @@ -texture-selector +texture-selector, type-selector, font-selector z-index 4 position fixed !important padding 1em \ No newline at end of file diff --git a/src/styl/tags/shared/asset-viewer.styl b/src/styl/tags/shared/asset-viewer.styl index abb2f499c..f1d93daf0 100644 --- a/src/styl/tags/shared/asset-viewer.styl +++ b/src/styl/tags/shared/asset-viewer.styl @@ -1,4 +1,10 @@ asset-viewer display block h1, h2 - margin-bottom 1rem \ No newline at end of file + margin-bottom 1rem + &.fonts + if (themeDark) + .cards li img + filter invert(1) + &:hover + background transparent \ No newline at end of file diff --git a/src/styl/tags/shared/type-selector.styl b/src/styl/tags/shared/type-selector.styl deleted file mode 100644 index 601a394c5..000000000 --- a/src/styl/tags/shared/type-selector.styl +++ /dev/null @@ -1,2 +0,0 @@ -type-selector - @extends texture-selector \ No newline at end of file From 4ee041ff6f2889fd7274a756e553c551931440c1 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 16:00:43 +1200 Subject: [PATCH 67/86] :sparkles: ct.camera now supports direct assignment for its scale, e.g. ct.camera.scale = 1.5; Closes #194 --- app/data/ct.release/camera.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/data/ct.release/camera.js b/app/data/ct.release/camera.js index dc49e5471..490cd63af 100644 --- a/app/data/ct.release/camera.js +++ b/app/data/ct.release/camera.js @@ -81,6 +81,19 @@ class Camera extends PIXI.DisplayObject { this.getBounds = this.getBoundingBox; } + get scale() { + return this.transform.scale; + } + set scale(value) { + if (typeof value === 'number') { + value = { + x: value, + y: value + }; + } + this.transform.scale.copyFrom(value); + } + /** * Moves the camera to a new position. It will have a smooth transition * if a `drift` parameter is set. From 3e52c9b866cead4107e39c7cb531aefdc38fd4c2 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 7 Aug 2020 16:49:53 +1200 Subject: [PATCH 68/86] :zap: Replace unzipper module and fix issues with module imports --- app/package-lock.json | 181 +++++++----------- app/package.json | 4 +- .../modules/modules-settings.tag | 125 ++++-------- 3 files changed, 111 insertions(+), 199 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 929d2dd98..3bda26f98 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1434,6 +1434,15 @@ "integrity": "sha512-g73GJYJDXgf0jqg+P9S8h2acWbDXNkoCX8DLtJVu7Fkn788pzQ/oJsrdJz/2JejRf/SjfZaAhsw+3nd1D5EWGg==", "optional": true }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1609,20 +1618,6 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -1676,16 +1671,6 @@ "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" }, - "buffer-indexof-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", - "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=" - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" - }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -1738,14 +1723,6 @@ "upper-case": "^1.1.1" } }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -1983,14 +1960,6 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "optional": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -2193,6 +2162,32 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "fast-plist": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz", @@ -2206,6 +2201,14 @@ "punycode": "^1.3.2" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -2290,17 +2293,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, "fuse.js": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", @@ -2360,17 +2352,12 @@ "read-pkg-up": "^2.0.0" } }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "pump": "^3.0.0" } }, "global-agent": { @@ -2619,11 +2606,6 @@ } } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" - }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -2983,6 +2965,11 @@ } } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -3096,6 +3083,15 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "optional": true }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -3181,14 +3177,6 @@ "lowercase-keys": "^1.0.0" } }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, "roarr": { "version": "2.15.2", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.2.tgz", @@ -3259,11 +3247,6 @@ "range-parser": "1.2.0" } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3408,11 +3391,6 @@ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" - }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -3469,30 +3447,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - } - } - }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", @@ -3568,7 +3522,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { @@ -3592,6 +3546,15 @@ } } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "zip-stream": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.2.tgz", diff --git a/app/package.json b/app/package.json index b2c25d77c..bb6a503b0 100644 --- a/app/package.json +++ b/app/package.json @@ -56,6 +56,7 @@ "archiver": "^3.1.1", "csswring": "7.0.0", "electron-packager": "^14.2.1", + "extract-zip": "^2.0.1", "fs-extra": "^9.0.1", "fuse.js": "^3.6.1", "google-closure-compiler": "^20191111.0.0", @@ -71,7 +72,6 @@ "pixi.js-legacy": "5.1.2", "png2icons": "^2.0.1", "serve-handler": "^6.1.3", - "ttf2woff": "^2.0.2", - "unzipper": "^0.10.11" + "ttf2woff": "^2.0.2" } } diff --git a/src/riotTags/project-settings/modules/modules-settings.tag b/src/riotTags/project-settings/modules/modules-settings.tag index 13c1b5786..d52d5a26b 100644 --- a/src/riotTags/project-settings/modules/modules-settings.tag +++ b/src/riotTags/project-settings/modules/modules-settings.tag @@ -61,105 +61,54 @@ modules-settings.panel.view this.filterCategories.splice(this.filterCategories.indexOf('default'), 1); this.categoryToIconMap = categoryToIconMap; - const path = require('path'), - fs = require('fs-extra'), - unzipper = require('unzipper'); - this.resortEnabledModules = () => { this.enabledModules = this.allModules .filter(module => module.name in global.currentProject.libs); }; - loadModules().then(modules => { - // Sort by their codename (folder name) - this.allModules = modules.sort((a, b) => a.name.localeCompare(b.name)); - this.categoriesCounter = {}; - for (const category of this.filterCategories) { - this.categoriesCounter[category] = 0; - } - for (const module of this.allModules) { - if (module.manifest.main.categories) { - for (const category of module.manifest.main.categories) { - if (this.filterCategories.includes(category)) { - this.categoriesCounter[category]++; + this.refreshModules = () => { + loadModules().then(modules => { + // Sort by their codename (folder name) + this.allModules = modules.sort((a, b) => a.name.localeCompare(b.name)); + this.categoriesCounter = {}; + for (const category of this.filterCategories) { + this.categoriesCounter[category] = 0; + } + for (const module of this.allModules) { + if (module.manifest.main.categories) { + for (const category of module.manifest.main.categories) { + if (this.filterCategories.includes(category)) { + this.categoriesCounter[category]++; + } } } } - } - this.resortEnabledModules(); - this.update(); - }); - + this.resortEnabledModules(); + this.update(); + }); + }; + this.refreshModules(); - this.importModules = e => { - const {files} = e.target; + this.importModules = async e => { + const files = [...e.target.files]; + e.target.value = ''; if (files.length === 0) { return; } - const value = files[0].path; - e.target.value = ''; - let parentName = null; - let moduleName = null; - const entries = []; - fs.createReadStream(value) - .pipe(unzipper.Parse()) - .on('entry', async entry => { - const fileName = entry.path.toLowerCase(); - if (path.basename(fileName) === 'module.json') { - // consume the entry by buffering the contents into memory. - // eslint-disable-next-line require-atomic-updates - const content = entry.tmpContent = await entry.buffer(); - const json = JSON.parse(content.toString()); - moduleName = json.main.packageName || path.basename(value, '.zip'); - const indexOf = fileName.indexOf('/'); - if (indexOf !== -1) { - parentName = fileName.substring(0, indexOf); - } - } - entries.push(entry); - }) - .on('finish', async () => { - if (moduleName !== null) { - // create a parent directory - await fs.ensureDir(path.join(moduleDir, moduleName)); - for (const entry of entries) { - const filePath = entry.path; - const indexOf = filePath.indexOf('/'); - if (filePath === parentName) { - continue; - } - if (indexOf !== -1) { - if (parentName !== null) { - entry.path = `${moduleName}${filePath.substring(indexOf)}`; - } else { - entry.path = `${moduleName}/${filePath}`; - } - } else { - entry.path = `${moduleName}/${filePath}`; - } - entry.path = path.join(moduleDir, entry.path); - // 'Directory' or 'File' - if (entry.type === 'Directory') { - // eslint-disable-next-line no-await-in-loop - await fs.ensureDir(entry.path); - } else { - const fileName = entry.path.toLowerCase(); - if (fileName.endsWith('module.json')) { - const content = entry.tmpContent; - // eslint-disable-next-line no-await-in-loop - await fs.writeFile(entry.path, content); - } else { - entry.pipe(fs.createWriteStream(entry.path)) - .on('error', (e) => { - alertify.error(e); - console.error(e); - }); - } - } - } - this.allModules.push(moduleName); - this.update(); - } - }); + + const path = require('path'), + fs = require('fs-extra'), + extract = require('extract-zip'); + + const unpackPromises = []; + + for (const file of files) { + const zip = file.path; + unpackPromises.push(extract(zip, { + dir: path.resolve(path.join(moduleDir, path.basename(zip, path.extname(zip)))) + })); + } + await Promise.all(unpackPromises); + this.refreshModules(); }; this.searchValue = ''; From b1f1292d54afe92c1c1d6c4f92622692988dc8bd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 11:13:30 +1200 Subject: [PATCH 69/86] :sparkles: Extensions support (moddable fields) for rooms --- app/data/ct.release/rooms.js | 5 ++- app/data/i18n/English.json | 1 + src/node_requires/exporter/rooms.js | 3 +- src/riotTags/rooms/room-editor.tag | 43 ++++++++++++++++------- src/riotTags/shared/extensions-editor.tag | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/data/ct.release/rooms.js b/app/data/ct.release/rooms.js index 307271dd6..d6e73ce0f 100644 --- a/app/data/ct.release/rooms.js +++ b/app/data/ct.release/rooms.js @@ -17,12 +17,16 @@ class Room extends PIXI.Container { ct.room = ct.rooms.current = this; } if (template) { + if (template.extends) { + ct.u.ext(this, template.extends); + } this.onCreate = template.onCreate; this.onStep = template.onStep; this.onDraw = template.onDraw; this.onLeave = template.onLeave; this.template = template; this.name = template.name; + /*%beforeroomoncreate%*/ for (let i = 0, li = template.bgs.length; i < li; i++) { const bg = new ct.types.Background( template.bgs[i].texture, @@ -277,7 +281,6 @@ class Room extends PIXI.Container { ct.roomHeight ); ct.pixiApp.renderer.resize(template.width, template.height); - /*%beforeroomoncreate%*/ ct.rooms.current = ct.room = new Room(template); ct.stage.addChild(ct.room); ct.room.onCreate(); diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 0c743c1d8..8426944ab 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -514,6 +514,7 @@ "copies": "Copies", "backgrounds": "Backgrounds", "tiles": "Tiles", + "properties": "Properties", "add": "Add", "none": "Nothing", "done": "Done", diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index 8ba9c287f..c0a10da2f 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -77,7 +77,8 @@ ct.rooms.templates['${r.name}'] = { }, onCreate() { ${proj.rooms[k].oncreate} - } + }, + extends: ${proj.rooms[k].extends ? JSON.stringify(getUnwrappedExtends(proj.rooms[k].extends), null, 4) : '{}'} }`; } return roomsCode; diff --git a/src/riotTags/rooms/room-editor.tag b/src/riotTags/rooms/room-editor.tag index e6c361904..2171a724e 100644 --- a/src/riotTags/rooms/room-editor.tag +++ b/src/riotTags/rooms/room-editor.tag @@ -5,15 +5,6 @@ room-editor.panel.view br input.wide(type="text" value="{room.name}" onchange="{wire('this.room.name')}") .anErrorNotice(if="{nameTaken}" ref="errorNotice") {vocGlob.nametaken} - .fifty.npt.npb.npl - b {voc.width} - br - input.wide(type="number" value="{room.width}" onchange="{wire('this.room.width')}") - .fifty.npt.npb.npr - b {voc.height} - br - input.wide(type="number" value="{room.height}" onchange="{wire('this.room.height')}") - br button.wide(onclick="{openRoomEvents}") svg.feather(if="{room.oncreate || room.onstep || room.ondestroy || room.ondraw}") use(xlink:href="data/icons.svg#check") @@ -21,13 +12,38 @@ room-editor.panel.view .palette .tabwrap ul.tabs.nav.noshrink.nogrow - li(onclick="{changeTab('roomcopies')}" class="{active: tab === 'roomcopies'}") {voc.copies} - li(onclick="{changeTab('roombackgrounds')}" class="{active: tab === 'roombackgrounds'}") {voc.backgrounds} - li(onclick="{changeTab('roomtiles')}" class="{active: tab === 'roomtiles'}") {voc.tiles} + li(onclick="{changeTab('roomcopies')}" title="{voc.copies}" class="{active: tab === 'roomcopies'}") + svg.feather + use(xlink:href="data/icons.svg#type") + span(show="{sidebarWidth > 500}") {voc.copies} + li(onclick="{changeTab('roombackgrounds')}" title="{voc.backgrounds}" class="{active: tab === 'roombackgrounds'}") + svg.feather + use(xlink:href="data/icons.svg#image") + span(show="{sidebarWidth > 500}") {voc.backgrounds} + li(onclick="{changeTab('roomtiles')}" title="{voc.tiles}" class="{active: tab === 'roomtiles'}") + svg.feather + use(xlink:href="data/icons.svg#texture") + span(show="{sidebarWidth > 500}") {voc.tiles} + li(onclick="{changeTab('properties')}" title="{voc.properties}" class="{active: tab === 'properties'}") + svg.feather + use(xlink:href="data/icons.svg#settings") + span(show="{sidebarWidth > 500}") {voc.properties} .relative room-type-picker(show="{tab === 'roomcopies'}" current="{currentType}") room-backgrounds-editor(show="{tab === 'roombackgrounds'}" room="{room}") room-tile-editor(show="{tab === 'roomtiles'}" room="{room}") + .pad.panel(show="{tab === 'properties'}") + .fifty.npt.npb.npl + b {voc.width} + br + input.wide(type="number" value="{room.width}" onchange="{wire('this.room.width')}") + .fifty.npt.npb.npr + b {voc.height} + br + input.wide(type="number" value="{room.height}" onchange="{wire('this.room.height')}") + .clear + extensions-editor(entity="{this.room.extends}" type="room" wide="aye" compact="sure") + .done.nogrow button.wide#roomviewdone(onclick="{roomSave}") svg.feather @@ -121,6 +137,9 @@ room-editor.panel.view this.mixin(window.roomTileTools); this.room = this.opts.room; + if (!this.room.extends) { + this.room.extends = {}; + } this.mouseX = this.mouseY = 0; this.roomx = this.room.width / 2; diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index bebe6350f..58e05b5a7 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -4,7 +4,7 @@ @attribute entity (riot object) An object to which apply editing to. - @attribute type (string, 'type'|'tileLayer') + @attribute type (string, 'type'|'tileLayer'|'room') The type of the edited asset. Not needed if customextends is set. @attribute [compact] (atomic) From 8614ff6c66de9b2250ba0cf9662092913baffa8d Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 11:18:32 +1200 Subject: [PATCH 70/86] :sparkles: Catmods: a "code" extension type for monospace text input --- app/data/ct.libs/akatemplate/module.json | 6 +++--- app/data/ct.libs/sprite/module.json | 2 +- src/riotTags/shared/extensions-editor.tag | 8 +++++++- src/styl/typography.styl | 3 +++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/data/ct.libs/akatemplate/module.json b/app/data/ct.libs/akatemplate/module.json index 4048b9169..3007da5b5 100644 --- a/app/data/ct.libs/akatemplate/module.json +++ b/app/data/ct.libs/akatemplate/module.json @@ -17,21 +17,21 @@ "key": "toptop", "id": "toptop", "default": " ", - "type": "textfield" + "type": "code" }, { "name": "HTML bottom", "key": "botbot", "id": "botbot", "default": " ", - "type": "textfield" + "type": "code" }, { "name": "CSS", "key": "csscss", "id": "csscss", "default": " ", - "type": "textfield" + "type": "code" } ] } diff --git a/app/data/ct.libs/sprite/module.json b/app/data/ct.libs/sprite/module.json index 2794a6d8d..375f24032 100644 --- a/app/data/ct.libs/sprite/module.json +++ b/app/data/ct.libs/sprite/module.json @@ -17,6 +17,6 @@ "id": "spritedef", "desc": "Place your `ct.sprite` calls here", "default": "", - "type": "textfield" + "type": "code" }] } diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index 58e05b5a7..cecf2b194 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -22,7 +22,7 @@ declare interface IExtensionField { name: string, // the displayed name. // Below 'h1', 'h2', 'h3', 'h4' are purely decorational, for grouping fields. Others denote the type of an input field. - type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textbox' | 'number' | 'point2D' | 'checkbox' | 'radio' | 'texture' | 'type', + type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textfield' | 'code' | 'number' | 'point2D' | 'checkbox' | 'radio' | 'texture' | 'type', key?: string, // the name of a JSON key to write into the `opts.entity`. Not needed for hN types, but required otherwise // The key may have special suffixes that tell the exporter to unwrap foreign keys (resources' UIDs) into asset names. // These are supposed to always be used with `'type'` and `'texture'` input types. @@ -105,6 +105,12 @@ extensions-editor value="{parent.opts.entity[ext.key] || ext.default}" onchange="{wire('this.opts.entity.'+ ext.key)}" ) + textarea.monospace( + if="{ext.type === 'code'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) input( if="{ext.type === 'number'}" class="{compact: parent.opts.compact, wide: parent.opts.wide}" diff --git a/src/styl/typography.styl b/src/styl/typography.styl index 2459888ea..4cee58b23 100644 --- a/src/styl/typography.styl +++ b/src/styl/typography.styl @@ -36,6 +36,9 @@ pre font-family font-mono overflow auto max-height 30rem +textarea.monospace + font-family font-mono + width 100% code font-family font-mono &.inline, p > &, li > & From 41767e662fc723086bdd4141eccf4f9e5f889928 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 11:58:01 +1200 Subject: [PATCH 71/86] :zap: Update russian UI translation --- app/data/i18n/Russian.json | 74 ++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 7ad31a450..6754787d8 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -50,7 +50,10 @@ "yes": "Да", "zoom": "Масштаб", "zoomIn": "Приблизить", - "zoomOut": "Уменьшить" + "zoomOut": "Уменьшить", + "filter": "Отфильтровать:", + "selectDialogue": "Выбрать…", + "search": "Поиск:" }, "colorPicker": { "old": "Старый", @@ -67,7 +70,7 @@ "debug": "Версия для отладки", "export": "Экспортировать", "exportPanel": "Экспорт проекта", - "firstrunnotice": "В первый запуск, ct.js скачает дополнительные файлы для каждой платформы. Это займёт время, но после этот процесс будет практически мгновенным.", + "firstrunnotice": "При первом экспорте ct.js скачает дополнительные файлы для каждой платформы. Это займёт время, но после этот процесс будет практически мгновенным.", "log": "Логи", "windowsCrossBuildWarning": "Чтобы паковать игры для Windows с Линукса или MacOS, нужно установить Wine. Инструкции по установке Wine разные от системы к системе, так что лучше погуглите сами :)", "cannotBuildForMacOnWin": "К сожалению, Windows может делать для маков только сломанные пакеты. Попробуйте сделать билд на линуксе — например, в виртуалке. Это бесплатно!" @@ -147,7 +150,11 @@ "disableAcceleration": "Отключить графическое ускорение (требует перезапуск)", "disableBuiltInDebugger": "Выключить встроенный отладчик игр", "visitDiscordForGamedevSupport": "Присоединиться к Discord-серверу для помощи в разработке", - "postAnIssue": "Зайти на GitHub" + "postAnIssue": "Зайти на GitHub", + "restart": "Перезапустить", + "themeLucasDracula": "Люкас Дракула", + "openProject": "Открыть проект…", + "openExample": "Открыть пример…" }, "onboarding": { "hoorayHeader": "Ух-ты! Мы сделали новый проект!", @@ -210,7 +217,11 @@ "moveDown": "Поставить ниже", "moveUp": "Поставить выше", "newScriptComment": "Используйте скрипты для создания функций и импорта небольших библиотек" - } + }, + "modules": { + "heading": "Котомоды" + }, + "catmodsSettings": "Настройки котомодов" }, "modules": { "author": "Автор котомода", @@ -219,17 +230,35 @@ "parameters": "Параметры", "info": "Инфо", "license": "Лицензия", - "logs": "Ченджлог", + "logs": "Лог изменений", "logs2": "Лог изменений", "settings": "Настройки", "importModules": "Импортировать модули", "dependencies": "Зависимости:", - "optionalDependencies": "Опциональные зависимости:" + "optionalDependencies": "Опциональные зависимости:", + "enabledModules": "Включенные модули", + "availableModules": "Доступные модули", + "filter": "Фильтр", + "categories": { + "customization": "Кастомизация", + "utilities": "Утилиты", + "media": "Медиа", + "misc": "Разное", + "desktop": "Десктоп-сборки", + "motionPlanning": "Планирование передвижения", + "inputs": "Методы ввода", + "fx": "Эффекты", + "mobile": "Мобильные устройства", + "integrations": "Интеграции", + "tweaks": "Настройки", + "networking": "Сеть" + } }, "texture": { "create": "Создать", "import": "Импорт", - "skeletons": "Скелетная анимация" + "skeletons": "Скелетная анимация", + "createType": "Создать тип с этой текстурой" }, "textureview": { "bgcolor": "Сменить цвет фона", @@ -311,7 +340,8 @@ "strokeweight": "Толщина обводки:", "testtext": "Тест Test 0123 +", "textWrap": "Перенос текста", - "textWrapWidth": "Позиция для переноса:" + "textWrapWidth": "Позиция для переноса:", + "useCustomFont": "Выбрать шрифт из проекта…" }, "fonts": { "fonts": "Шрифты", @@ -322,7 +352,23 @@ "typefacename": "Название семейства:", "fontweight": "Жирность:", "italic": "Наклонный стиль?", - "reimport": "Заменить" + "reimport": "Заменить", + "generateBitmapFont": "Т.ж. сгенерировать bitmap-шрифт", + "bitmapFont": "Bitmap-шрифт", + "bitmapFontSize": "Размер шрифта:", + "bitmapFontLineHeight": "Высота линии:", + "resultingBitmapFontName": "Название ресурса", + "charset": "Кодировки:", + "charsets": { + "punctuation": "Цифры и пунктуация (обычно вам это нужно)", + "basicLatin": "Латиница", + "latinExtended": "Расширенная латиница", + "cyrillic": "Кириллица", + "greekCoptic": "Греческий и Коптский", + "custom": "Свой набор", + "allInFont": "Всё, что поддерживает шрифт" + }, + "customCharsetHint": "Впишите все буквы, которые нужно добавить в шрифт — как в верхнем, так и в нижнем регистре." }, "particleEmitters": { "emittersHeading": "Системы частиц", @@ -461,13 +507,15 @@ "findTileset": "Найти тайлсет", "movetilestolayer": "Переместить в другой слой", "shifttiles": "Сместить плитки", - "changecopyrotation": "Повернуть" + "changecopyrotation": "Повернуть", + "properties": "Свойства" }, "notepad": { "local": "Блокнот проекта", "global": "Общий блокнот", "helppages": "Справка", - "backToHome": "Назад на главную документации" + "backToHome": "Назад на главную документации", + "modulespages": "Документация к модулям" }, "patreon": { "patronsHeader": "Наши покровители", @@ -524,5 +572,9 @@ "links": "Ссылки и QR-коды", "openExternal": "Открыть в браузере", "close": "Закрыть" + }, + "docsPanel": { + "documentation": "Документация", + "reference": "Список методов" } } From 48c3e496a1d320bdc80b90b8369b0890c83a9a63 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 11:58:19 +1200 Subject: [PATCH 72/86] :zap: Updates debug translation file and comments file --- app/data/i18n/Comments.json | 193 +++++++++++++++++++++-------------- app/data/i18n/Debug.json | 194 ++++++++++++++++++++++-------------- 2 files changed, 238 insertions(+), 149 deletions(-) diff --git a/app/data/i18n/Comments.json b/app/data/i18n/Comments.json index 09379b7cb..c7fde00b4 100644 --- a/app/data/i18n/Comments.json +++ b/app/data/i18n/Comments.json @@ -49,20 +49,11 @@ "select": "", "zoom": "", "zoomIn": "", - "zoomOut": "" - }, - "actionsEditor": { - "actions": "", - "actionsEditor": "", - "addAction": "", - "addMethod": "", - "deleteAction": "", - "deleteMethod": "", - "inputActionNamePlaceholder": "", - "methodModuleMissing": "", - "methods": "", - "multiplier": "", - "noActionsYet": "" + "zoomOut": "", + "clear": "", + "filter": "A verb", + "selectDialogue": "A verb", + "search": "Shown next to a search field" }, "colorPicker": { "current": "", @@ -82,17 +73,8 @@ "firstrunnotice": "", "log": "", "windowsCrossBuildWarning": "", - "launchMode": "A label before options that set initial game state: maximized, windowed, full-screen", - "launchModes": { - "maximized": "", - "fullscreen": "", - "windowed": "" - }, "cannotBuildForMacOnWin": "" }, - "inputMethodSelector": { - "select": "" - }, "intro": { "loading": "", "newProject": { @@ -131,12 +113,10 @@ "launch": "", "license": "", "min": "", - "modules": "", "recentProjects": "", "rooms": "", "save": "", "startScreen": "", - "settings": "", "sounds": "", "successZipExport": "", "successZipProject": "", @@ -167,52 +147,18 @@ "disableAcceleration": "", "disableBuiltInDebugger": "", "visitDiscordForGamedevSupport": "", - "postAnIssue": "" - }, - "settings": { - "actions": "", - "author": "", - "authoring": "", - "cover": "", - "editActions": "", - "exportparams": "", - "framerate": "", - "getfile": "", - "highDensity": "", - "maxFPS": "", - "minifyhtmlcss": "", - "minifyjs": "", - "pixelatedrender": "", - "renderoptions": "", + "postAnIssue": "", + "restart": "", + "project": "", "settings": "", - "site": "", - "title": "", - "version": "", - "versionpostfix": "", - "scripts": { - "addNew": "", - "deleteScript": "", - "header": "", - "newScriptComment": "", - "moveUp": "", - "moveDown": "" - }, - "usePixiLegacy": "", - "branding": { - "heading": "", - "icon": "", - "iconNotice": "", - "accent": "", - "accentNotice": "", - "invertPreloaderScheme": "" - } + "themeSpringSream": "", + "themeLucasDracula": "", + "openProject": "", + "openExample": "" }, "modules": { "author": "", - "hasfields": "", - "hasinjects": "", - "hasinputmethods": "", - "help": "", + "help": "As a documentation page that has all the methods and properties described", "info": "", "license": "", "logs": "A title of a tab that shows a changelof of a module, when clicked. It can be shorter than modules.log2", @@ -222,12 +168,30 @@ "settings": "", "dependencies": "", "optionalDependencies": "", - "importModules": "" + "importModules": "", + "enabledModules": "", + "availableModules": "", + "filter": "A noun; a title of the collapsible section with a filtering form inside it", + "categories": { + "customization": "", + "utilities": "", + "media": "", + "misc": "As \"miscellaneous\"; a noun", + "desktop": "", + "motionPlanning": "", + "inputs": "", + "fx": "", + "mobile": "", + "integrations": "", + "tweaks": "", + "networking": "" + } }, "texture": { "create": "", "import": "A verb", - "skeletons": "" + "skeletons": "", + "createType": "" }, "textureview": { "bgcolor": "", @@ -309,7 +273,8 @@ "strokeweight": "", "testtext": "It is recommended that you include both a string in your language and in English, as it will help with picking fonts that have multilingual support.", "textWrap": "", - "textWrapWidth": "" + "textWrapWidth": "", + "useCustomFont": "Can be worded differently to become shorter; this is a label of a button that opens a dialog with imported TTF fonts." }, "fonts": { "fonts": "", @@ -320,7 +285,23 @@ "typefacename": "", "fontweight": "", "italic": "", - "reimport": "" + "reimport": "", + "generateBitmapFont": "", + "bitmapFont": "", + "bitmapFontSize": "", + "bitmapFontLineHeight": "", + "resultingBitmapFontName": "", + "charset": "", + "charsets": { + "punctuation": "", + "basicLatin": "", + "latinExtended": "", + "cyrillic": "", + "greekCoptic": "", + "custom": "Describing a custom set of symbols", + "allInFont": "" + }, + "customCharsetHint": "" }, "types": { "create": "" @@ -391,13 +372,15 @@ "movetilestolayer": "", "shifttiles": "", "findTileset": "", - "changecopyrotation": "" + "changecopyrotation": "", + "properties": "" }, "notepad": { "local": "", "global": "", "helppages": "", - "backToHome": "" + "backToHome": "", + "modulespages": "" }, "preview": {}, "curveEditor": { @@ -532,5 +515,67 @@ "", "" ] + }, + "docsPanel": { + "documentation": "", + "reference": "A document with all the properties and methods described in it" + }, + "settings": { + "actions": { + "heading": "", + "actions": "", + "addAction": "", + "addMethod": "", + "deleteAction": "", + "deleteMethod": "", + "inputActionNamePlaceholder": "", + "methodModuleMissing": "", + "methods": "", + "multiplier": "", + "noActionsYet": "" + }, + "authoring": { + "heading": "", + "author": "", + "site": "", + "title": "", + "version": "", + "versionpostfix": "" + }, + "branding": { + "heading": "", + "accent": "", + "accentNotice": "", + "icon": "", + "iconNotice": "", + "invertPreloaderScheme": "" + }, + "modules": { + "heading": "" + }, + "rendering": { + "heading": "", + "framerate": "", + "highDensity": "", + "maxFPS": "", + "pixelatedrender": "", + "usePixiLegacy": "", + "desktopBuilds": "", + "launchMode": "", + "launchModes": { + "maximized": "", + "fullscreen": "", + "windowed": "" + } + }, + "scripts": { + "heading": "", + "addNew": "", + "deleteScript": "", + "moveDown": "", + "moveUp": "", + "newScriptComment": "" + }, + "catmodsSettings": "" } -} \ No newline at end of file +} diff --git a/app/data/i18n/Debug.json b/app/data/i18n/Debug.json index a80a911d6..1d2212b34 100644 --- a/app/data/i18n/Debug.json +++ b/app/data/i18n/Debug.json @@ -49,20 +49,11 @@ "select": "common.select", "zoom": "common.zoom", "zoomIn": "common.zoomIn", - "zoomOut": "common.zoomOut" - }, - "actionsEditor": { - "actions": "actionsEditor.actions", - "actionsEditor": "actionsEditor.actionsEditor", - "addAction": "actionsEditor.addAction", - "addMethod": "actionsEditor.addMethod", - "deleteAction": "actionsEditor.deleteAction", - "deleteMethod": "actionsEditor.deleteMethod", - "inputActionNamePlaceholder": "actionsEditor.inputActionNamePlaceholder", - "methodModuleMissing": "actionsEditor.methodModuleMissing", - "methods": "actionsEditor.methods", - "multiplier": "actionsEditor.multiplier", - "noActionsYet": "actionsEditor.noActionsYet" + "zoomOut": "common.zoomOut", + "clear": "common.clear", + "filter": "common.filter", + "selectDialogue": "common.selectDialogue", + "search": "common.search" }, "colorPicker": { "current": "colorPicker.current", @@ -82,17 +73,8 @@ "firstrunnotice": "exportPanel.firstrunnotice", "log": "exportPanel.log", "windowsCrossBuildWarning": "exportPanel.windowsCrossBuildWarning", - "launchMode": "exportPanel.launchMode", - "launchModes": { - "maximized": "exportPanel.launchModes.maximized", - "fullscreen": "exportPanel.launchModes.fullscreen", - "windowed": "exportPanel.launchModes.windowed" - }, "cannotBuildForMacOnWin": "exportPanel.cannotBuildForMacOnWin" }, - "inputMethodSelector": { - "select": "inputMethodSelector.select" - }, "intro": { "loading": "intro.loading", "newProject": { @@ -131,12 +113,10 @@ "launch": "menu.launch", "license": "menu.license", "min": "menu.min", - "modules": "menu.modules", "recentProjects": "menu.recentProjects", "rooms": "menu.rooms", "save": "menu.save", "startScreen": "menu.startScreen", - "settings": "menu.settings", "sounds": "menu.sounds", "successZipExport": "menu.successZipExport", "successZipProject": "menu.successZipProject", @@ -167,51 +147,17 @@ "disableAcceleration": "menu.disableAcceleration", "disableBuiltInDebugger": "menu.disableBuiltInDebugger", "visitDiscordForGamedevSupport": "menu.visitDiscordForGamedevSupport", - "postAnIssue": "menu.postAnIssue" - }, - "settings": { - "actions": "settings.actions", - "author": "settings.author", - "authoring": "settings.authoring", - "cover": "settings.cover", - "editActions": "settings.editActions", - "exportparams": "settings.exportparams", - "framerate": "settings.framerate", - "getfile": "settings.getfile", - "highDensity": "settings.highDensity", - "maxFPS": "settings.maxFPS", - "minifyhtmlcss": "settings.minifyhtmlcss", - "minifyjs": "settings.minifyjs", - "pixelatedrender": "settings.pixelatedrender", - "renderoptions": "settings.renderoptions", - "settings": "settings.settings", - "site": "settings.site", - "title": "settings.title", - "version": "settings.version", - "versionpostfix": "settings.versionpostfix", - "scripts": { - "addNew": "settings.scripts.addNew", - "deleteScript": "settings.scripts.deleteScript", - "header": "settings.scripts.header", - "newScriptComment": "settings.scripts.newScriptComment", - "moveUp": "settings.scripts.moveUp", - "moveDown": "settings.scripts.moveDown" - }, - "usePixiLegacy": "settings.usePixiLegacy", - "branding": { - "heading": "settings.branding.heading", - "icon": "settings.branding.icon", - "iconNotice": "settings.branding.iconNotice", - "accent": "settings.branding.accent", - "accentNotice": "settings.branding.accentNotice", - "invertPreloaderScheme": "settings.branding.invertPreloaderScheme" - } + "postAnIssue": "menu.postAnIssue", + "restart": "menu.restart", + "project": "menu.project", + "settings": "menu.settings", + "themeSpringSream": "menu.themeSpringSream", + "themeLucasDracula": "menu.themeLucasDracula", + "openProject": "menu.openProject", + "openExample": "menu.openExample" }, "modules": { "author": "modules.author", - "hasfields": "modules.hasfields", - "hasinjects": "modules.hasinjects", - "hasinputmethods": "modules.hasinputmethods", "help": "modules.help", "info": "modules.info", "license": "modules.license", @@ -222,12 +168,30 @@ "settings": "modules.settings", "dependencies": "modules.dependencies", "optionalDependencies": "modules.optionalDependencies", - "importModules": "modules.importModules" + "importModules": "modules.importModules", + "enabledModules": "modules.enabledModules", + "availableModules": "modules.availableModules", + "filter": "modules.filter", + "categories": { + "customization": "modules.categories.customization", + "utilities": "modules.categories.utilities", + "media": "modules.categories.media", + "misc": "modules.categories.misc", + "desktop": "modules.categories.desktop", + "motionPlanning": "modules.categories.motionPlanning", + "inputs": "modules.categories.inputs", + "fx": "modules.categories.fx", + "mobile": "modules.categories.mobile", + "integrations": "modules.categories.integrations", + "tweaks": "modules.categories.tweaks", + "networking": "modules.categories.networking" + } }, "texture": { "create": "texture.create", "import": "texture.import", - "skeletons": "texture.skeletons" + "skeletons": "texture.skeletons", + "createType": "texture.createType" }, "textureview": { "bgcolor": "textureview.bgcolor", @@ -309,7 +273,8 @@ "strokeweight": "styleview.strokeweight", "testtext": "styleview.testtext", "textWrap": "styleview.textWrap", - "textWrapWidth": "styleview.textWrapWidth" + "textWrapWidth": "styleview.textWrapWidth", + "useCustomFont": "styleview.useCustomFont" }, "fonts": { "fonts": "fonts.fonts", @@ -320,7 +285,23 @@ "typefacename": "fontview.typefacename", "fontweight": "fontview.fontweight", "italic": "fontview.italic", - "reimport": "fontview.reimport" + "reimport": "fontview.reimport", + "generateBitmapFont": "fontview.generateBitmapFont", + "bitmapFont": "fontview.bitmapFont", + "bitmapFontSize": "fontview.bitmapFontSize", + "bitmapFontLineHeight": "fontview.bitmapFontLineHeight", + "resultingBitmapFontName": "fontview.resultingBitmapFontName", + "charset": "fontview.charset", + "charsets": { + "punctuation": "fontview.charsets.punctuation", + "basicLatin": "fontview.charsets.basicLatin", + "latinExtended": "fontview.charsets.latinExtended", + "cyrillic": "fontview.charsets.cyrillic", + "greekCoptic": "fontview.charsets.greekCoptic", + "custom": "fontview.charsets.custom", + "allInFont": "fontview.charsets.allInFont" + }, + "customCharsetHint": "fontview.customCharsetHint" }, "types": { "create": "types.create" @@ -391,15 +372,16 @@ "movetilestolayer": "roomview.movetilestolayer", "shifttiles": "roomview.shifttiles", "findTileset": "roomview.findTileset", - "changecopyrotation": "roomview.changecopyrotation" + "changecopyrotation": "roomview.changecopyrotation", + "properties": "roomview.properties" }, "notepad": { "local": "notepad.local", "global": "notepad.global", "helppages": "notepad.helppages", - "backToHome": "notepad.backToHome" + "backToHome": "notepad.backToHome", + "modulespages": "notepad.modulespages" }, - "preview": {}, "curveEditor": { "curveLineHint": "curveEditor.curveLineHint", "dragPointHint": "curveEditor.dragPointHint", @@ -532,5 +514,67 @@ "patreon.aboutFillers.15", "patreon.aboutFillers.16" ] + }, + "docsPanel": { + "documentation": "docsPanel.documentation", + "reference": "docsPanel.reference" + }, + "settings": { + "actions": { + "heading": "settings.actions.heading", + "actions": "settings.actions.actions", + "addAction": "settings.actions.addAction", + "addMethod": "settings.actions.addMethod", + "deleteAction": "settings.actions.deleteAction", + "deleteMethod": "settings.actions.deleteMethod", + "inputActionNamePlaceholder": "settings.actions.inputActionNamePlaceholder", + "methodModuleMissing": "settings.actions.methodModuleMissing", + "methods": "settings.actions.methods", + "multiplier": "settings.actions.multiplier", + "noActionsYet": "settings.actions.noActionsYet" + }, + "authoring": { + "heading": "settings.authoring.heading", + "author": "settings.authoring.author", + "site": "settings.authoring.site", + "title": "settings.authoring.title", + "version": "settings.authoring.version", + "versionpostfix": "settings.authoring.versionpostfix" + }, + "branding": { + "heading": "settings.branding.heading", + "accent": "settings.branding.accent", + "accentNotice": "settings.branding.accentNotice", + "icon": "settings.branding.icon", + "iconNotice": "settings.branding.iconNotice", + "invertPreloaderScheme": "settings.branding.invertPreloaderScheme" + }, + "modules": { + "heading": "settings.modules.heading" + }, + "rendering": { + "heading": "settings.rendering.heading", + "framerate": "settings.rendering.framerate", + "highDensity": "settings.rendering.highDensity", + "maxFPS": "settings.rendering.maxFPS", + "pixelatedrender": "settings.rendering.pixelatedrender", + "usePixiLegacy": "settings.rendering.usePixiLegacy", + "desktopBuilds": "settings.rendering.desktopBuilds", + "launchMode": "settings.rendering.launchMode", + "launchModes": { + "maximized": "settings.rendering.launchModes.maximized", + "fullscreen": "settings.rendering.launchModes.fullscreen", + "windowed": "settings.rendering.launchModes.windowed" + } + }, + "scripts": { + "heading": "settings.scripts.heading", + "addNew": "settings.scripts.addNew", + "deleteScript": "settings.scripts.deleteScript", + "moveDown": "settings.scripts.moveDown", + "moveUp": "settings.scripts.moveUp", + "newScriptComment": "settings.scripts.newScriptComment" + }, + "catmodsSettings": "settings.catmodsSettings" } -} \ No newline at end of file +} From 8ec9a26c53e2e58de19df92793b352c3bce50a7d Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 12:00:01 +1200 Subject: [PATCH 73/86] :bug: Fix linter warnings --- src/riotTags/debugger/debugger-screen-embedded.tag | 2 +- src/riotTags/project-settings/modules/modules-settings.tag | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/riotTags/debugger/debugger-screen-embedded.tag b/src/riotTags/debugger/debugger-screen-embedded.tag index ab61e4bca..9db8205cb 100644 --- a/src/riotTags/debugger/debugger-screen-embedded.tag +++ b/src/riotTags/debugger/debugger-screen-embedded.tag @@ -115,7 +115,7 @@ debugger-screen-embedded(class="{opts.class} {flexrow: verticalLayout, flexcol: /* Bootstrap preview and debug views */ this.on('mount', () => { - this.refs.gameView.addEventListener('permissionrequest', function(e) { + this.refs.gameView.addEventListener('permissionrequest', function permissionrequest(e) { if (e.permission === 'fullscreen') { e.request.allow(); } diff --git a/src/riotTags/project-settings/modules/modules-settings.tag b/src/riotTags/project-settings/modules/modules-settings.tag index d52d5a26b..c9ff10336 100644 --- a/src/riotTags/project-settings/modules/modules-settings.tag +++ b/src/riotTags/project-settings/modules/modules-settings.tag @@ -96,8 +96,7 @@ modules-settings.panel.view } const path = require('path'), - fs = require('fs-extra'), - extract = require('extract-zip'); + extract = require('extract-zip'); const unpackPromises = []; From 60c961986d66f0a636331895345ff34e207e69ec Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 12:02:25 +1200 Subject: [PATCH 74/86] :bug: Fix linter warnings at ct.howler catmod --- app/data/ct.libs/sound.howler/index.js | 55 +++++++++++++++----------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/app/data/ct.libs/sound.howler/index.js b/app/data/ct.libs/sound.howler/index.js index 4901c2fa4..978caae05 100644 --- a/app/data/ct.libs/sound.howler/index.js +++ b/app/data/ct.libs/sound.howler/index.js @@ -1,5 +1,5 @@ /* global Howler Howl */ -(function () { +(function ctHowler() { ct.sound = {}; ct.sound.howler = Howler; Howler.orientation(0, -1, 0, 0, 0, 1); @@ -7,12 +7,17 @@ ct.sound.howl = Howl; var defaultMaxDistance = [/*%defaultMaxDistance%*/][0] || 2500; - ct.sound.useDepth = [/*%useDepth%*/][0] === void 0? false : [/*%useDepth%*/][0]; - ct.sound.manageListenerPosition = [/*%manageListenerPosition%*/][0] === void 0? true : [/*%manageListenerPosition%*/][0]; + ct.sound.useDepth = [/*%useDepth%*/][0] === void 0 ? + false : + [/*%useDepth%*/][0]; + ct.sound.manageListenerPosition = [/*%manageListenerPosition%*/][0] === void 0 ? + true : + [/*%manageListenerPosition%*/][0]; /** * Detects if a particular codec is supported in the system - * @param {string} type One of: "mp3", "mpeg", "opus", "ogg", "oga", "wav", "aac", "caf", m4a", "mp4", "weba", "webm", "dolby", "flac". + * @param {string} type One of: "mp3", "mpeg", "opus", "ogg", "oga", "wav", + * "aac", "caf", m4a", "mp4", "weba", "webm", "dolby", "flac". * @returns {boolean} true/false */ ct.sound.detect = Howler.codecs; @@ -21,7 +26,8 @@ * Creates a new Sound object and puts it in resource object * * @param {string} name Sound's name - * @param {object} formats A collection of sound files of specified extension, in format `extension: path` + * @param {object} formats A collection of sound files of specified extension, + * in format `extension: path` * @param {string} [formats.ogg] Local path to the sound in ogg format * @param {string} [formats.wav] Local path to the sound in wav format * @param {string} [formats.mp3] Local path to the sound in mp3 format @@ -29,7 +35,7 @@ * * @returns {object} Sound's object */ - ct.sound.init = function (name, formats, options) { + ct.sound.init = function init(name, formats, options) { options = options || {}; var sounds = []; if (formats.wav && formats.wav.slice(-4) === '.wav') { @@ -76,7 +82,7 @@ maxDistance: opts.maxDistance || defaultMaxDistance, refDistance: opts.refDistance || 1, rolloffFactor: opts.rolloffFactor || 1, - panningModel: opts.panningModel || 'HRTF', + panningModel: opts.panningModel || 'HRTF' }, id); }; /** @@ -88,7 +94,7 @@ * * @returns {number} The ID of the created sound. This can be passed to Howler methods. */ - ct.sound.spawn = function(name, opts, cb) { + ct.sound.spawn = function spawn(name, opts, cb) { opts = opts || {}; if (typeof opts === 'function') { cb = opts; @@ -110,7 +116,7 @@ howl.pos(opts.x, opts.y || 0, opts.z || 0, id); } else { const copy = opts.position; - howl.pos(copy.x, copy.y, opts.z || (ct.sound.useDepth? copy.depth : 0), id); + howl.pos(copy.x, copy.y, opts.z || (ct.sound.useDepth ? copy.depth : 0), id); } set3Dparameters(howl, opts, id); } @@ -127,7 +133,7 @@ * @param {number} [id] An optional ID of a particular sound * @returns {void} */ - ct.sound.stop = function(name, id) { + ct.sound.stop = function stop(name, id) { ct.res.sounds[name].stop(id); }; @@ -138,7 +144,7 @@ * @param {number} [id] An optional ID of a particular sound * @returns {void} */ - ct.sound.pause = function(name, id) { + ct.sound.pause = function pause(name, id) { ct.res.sounds[name].pause(id); }; @@ -149,7 +155,7 @@ * @param {number} [id] An optional ID of a particular sound * @returns {void} */ - ct.sound.resume = function(name, id) { + ct.sound.resume = function resume(name, id) { ct.res.sounds[name].play(id); }; /** @@ -160,7 +166,7 @@ * @param {number} [id] An optional ID of a particular sound * @returns {boolean} `true` if the sound is playing, `false` otherwise. */ - ct.sound.playing = function(name, id) { + ct.sound.playing = function playing(name, id) { return ct.res.sounds[name].playing(id); }; /** @@ -170,7 +176,7 @@ * @param {string} name The name of a sound * @returns {void} */ - ct.sound.load = function(name) { + ct.sound.load = function load(name) { ct.res.sounds[name].load(); }; @@ -179,12 +185,13 @@ * Changes/returns the volume of the given sound. * * @param {string} name The name of a sound to affect. - * @param {number} [volume] The new volume from `0.0` to `1.0`. If empty, will return the existing volume. + * @param {number} [volume] The new volume from `0.0` to `1.0`. + * If empty, will return the existing volume. * @param {number} [id] If specified, then only the given sound instance is affected. * * @returns {number} The current volume of the sound. */ - ct.sound.volume = function (name, volume, id) { + ct.sound.volume = function volume(name, volume, id) { return ct.res.sounds[name].volume(volume, id); }; @@ -198,9 +205,9 @@ * * @returns {void} */ - ct.sound.fade = function(name, newVolume, duration, id) { + ct.sound.fade = function fade(name, newVolume, duration, id) { var howl = ct.res.sounds[name], - oldVolume = id? howl.volume(id) : howl.volume; + oldVolume = id ? howl.volume(id) : howl.volume; howl.fade(oldVolume, newVolume, duration, id); }; @@ -215,7 +222,7 @@ * * @returns {void} */ - ct.sound.moveListener = function(x, y, z) { + ct.sound.moveListener = function moveListener(x, y, z) { Howler.pos(x, y, z || 0); }; @@ -223,14 +230,15 @@ * Moves a 3D sound to a new location * * @param {string} name The name of a sound to move - * @param {number} id The ID of a particular sound. Pass `null` if you want to affect all the sounds of a given name. + * @param {number} id The ID of a particular sound. + * Pass `null` if you want to affect all the sounds of a given name. * @param {number} x The new x coordinate * @param {number} y The new y coordinate * @param {number} [z] The new z coordinate * * @returns {void} */ - ct.sound.position = function(name, id, x, y, z) { + ct.sound.position = function position(name, id, x, y, z) { var howl = ct.res.sounds[name], oldPosition = howl.pos(id); howl.pos(x, y, z || oldPosition[2], id); @@ -238,13 +246,14 @@ /** * Get/set the global volume for all sounds, relative to their own volume. - * @param {number} [volume] The new volume from `0.0` to `1.0`. If omitted, will return the current global volume. + * @param {number} [volume] The new volume from `0.0` to `1.0`. + * If omitted, will return the current global volume. * * @returns {number} The current volume. */ ct.sound.globalVolume = Howler.volume.bind(Howler); - ct.sound.exists = function(name) { + ct.sound.exists = function exists(name) { return (name in ct.res.sounds); }; })(); From 9cf3c01e985306c8327a797366ddae4e53394f0a Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 12:09:35 +1200 Subject: [PATCH 75/86] :bug: Add `scripts` to the default project; fixes "project.scripts is not iterable" error at compilation --- src/node_requires/resources/projects/defaultProject.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node_requires/resources/projects/defaultProject.js b/src/node_requires/resources/projects/defaultProject.js index 90a4dadf7..05e70b317 100644 --- a/src/node_requires/resources/projects/defaultProject.js +++ b/src/node_requires/resources/projects/defaultProject.js @@ -22,6 +22,7 @@ const defaultProjectTemplate = { rooms: [], actions: [], emitterTandems: [], + scripts: [], starting: 0, settings: { authoring: { From ea77e91ae35a6a329e0e469c3b8f394e8b5c05a1 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 12:56:21 +1200 Subject: [PATCH 76/86] :bento: Update Howler.js to v2.2.0 --- app/data/ct.libs/sound.howler/includes/howler.min.js | 10 +++++----- app/data/ct.libs/sound.howler/injects/afterroomdraw.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/data/ct.libs/sound.howler/includes/howler.min.js b/app/data/ct.libs/sound.howler/includes/howler.min.js index 25e7c880a..b752c93c8 100644 --- a/app/data/ct.libs/sound.howler/includes/howler.min.js +++ b/app/data/ct.libs/sound.howler/includes/howler.min.js @@ -1,6 +1,6 @@ -/*! howler.js v2.1.2 | (c) 2013-2019, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ -!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!o.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(var t=0;t0?i._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(i._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3,p=!(!i._loop&&!t._sprite[e][2]);i._sprite=e,i._ended=!1;var m=function(){i._paused=!1,i._seek=_,i._start=c,i._stop=f,i._loop=p};if(_>=f)return void t._ended(i);var v=i._node;if(t._webAudio){var h=function(){t._playLock=!1,m(),t._refreshBuffer(i);var e=i._muted||t._muted?0:i._volume;v.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===v.bufferSource.start?i._loop?v.bufferSource.noteGrainOn(0,_,86400):v.bufferSource.noteGrainOn(0,_,s):i._loop?v.bufferSource.start(0,_,86400):v.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l)),o||setTimeout(function(){t._emit("play",i._id),t._loadQueue()},0)};"running"===n.state?h():(t._playLock=!0,t.once("resume",h),t._clearTimer(i._id))}else{var y=function(){v.currentTime=_,v.muted=i._muted||t._muted||n._muted||v.muted,v.volume=i._volume*n.volume(),v.playbackRate=i._rate;try{var r=v.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,m(),r.then(function(){t._playLock=!1,v._unlocked=!0,o||(t._emit("play",i._id),t._loadQueue())}).catch(function(){t._playLock=!1,t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),i._ended=!0,i._paused=!0})):o||(t._playLock=!1,m(),t._emit("play",i._id),t._loadQueue()),v.playbackRate=i._rate,v.paused)return void t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||i._loop?t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l):(t._endTimers[i._id]=function(){t._ended(i),v.removeEventListener("ended",t._endTimers[i._id],!1)},v.addEventListener("ended",t._endTimers[i._id],!1))}catch(e){t._emit("playerror",i._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===v.src&&(v.src=t._src,v.load());var g=window&&window.ejecta||!v.readyState&&n._navigator.isCocoonJS;if(v.readyState>=3||g)y();else{t._playLock=!0;var A=function(){y(),v.removeEventListener(n._canPlayEvent,A,!1)};v.addEventListener(n._canPlayEvent,A,!1),t._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),i+=d*r,i=Math.max(0,i),i=Math.min(1,i),i=Math.round(100*i)/100,u._webAudio?e._volume=i:u.volume(i,e._id,!0),a&&(u._volume=i),(on&&i>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var i;if("number"!=typeof e)return i=t._soundById(o),i?i._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var d=0;d=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return t;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var i=t._soundById(o);if(i){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var d=t.playing(o)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),i._seek=e,i._ended=!1,t._clearTimer(o),t._webAudio||!i._node||isNaN(i._node.duration)||(i._node.currentTime=e);var l=function(){t._emit("seek",o),s&&t.play(o,!0)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._node.src=o._src,e._node.preload="auto",e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void d(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,d(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());(n._navigator&&n._navigator.standalone&&!r||n._navigator&&!n._navigator.standalone&&!r)&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:1,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t):"undefined"!=typeof global&&(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t)}(); +/*! howler.js v2.2.0 | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ +!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!o.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?i._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(i._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;i._sprite=e,i._ended=!1;var p=function(){i._paused=!1,i._seek=_,i._start=c,i._stop=f,i._loop=!(!i._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(i);var m=i._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(i);var e=i._muted||t._muted?0:i._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?i._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):i._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l)),o||setTimeout(function(){t._emit("play",i._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(i._id))}else{var h=function(){m.currentTime=_,m.muted=i._muted||t._muted||n._muted||m.muted,m.volume=i._volume*n.volume(),m.playbackRate=i._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o||(t._emit("play",i._id),t._loadQueue())}).catch(function(){t._playLock=!1,t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),i._ended=!0,i._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",i._id),t._loadQueue()),m.playbackRate=i._rate,m.paused)return void t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||i._loop?t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l):(t._endTimers[i._id]=function(){t._ended(i),m.removeEventListener("ended",t._endTimers[i._id],!1)},m.addEventListener("ended",t._endTimers[i._id],!1))}catch(e){t._emit("playerror",i._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0;var g=function(){h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),i+=d*r,i=d<0?Math.max(o,i):Math.min(o,i),i=Math.round(100*i)/100,u._webAudio?e._volume=i:u.volume(i,e._id,!0),a&&(u._volume=i),(on&&i>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var i;if("number"!=typeof e)return i=t._soundById(o),i?i._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var d=0;d=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return t;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var i=t._soundById(o);if(i){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var d=t.playing(o)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),i._seek=e,i._ended=!1,t._clearTimer(o),t._webAudio||!i._node||isNaN(i._node.duration)||(i._node.currentTime=e);var l=function(){t._emit("seek",o),s&&t.play(o,!0)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void d(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,d(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); /*! Spatial Plugin */ -!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var t=n._howls.length-1;t>=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(t,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(t,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a Date: Sun, 9 Aug 2020 13:16:30 +1200 Subject: [PATCH 77/86] :zap: Add a module's name to its settings page as a header --- src/riotTags/project-settings/project-settings.tag | 1 + 1 file changed, 1 insertion(+) diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag index d501e3f36..b11f584d4 100644 --- a/src/riotTags/project-settings/project-settings.tag +++ b/src/riotTags/project-settings/project-settings.tag @@ -36,6 +36,7 @@ project-settings.panel.view.pad.flexrow // This outputs a templated tag name. Magic! #{name + '-settings'} .pad(if="{currentModule}") + h1 {currentModule.manifest.main.name} extensions-editor(customextends="{currentModule.manifest.fields}" entity="{global.currentProject.libs[currentModule.name]}") script. this.namespace = 'settings'; From d8042b1b685420cdb47bcb7cebbb849325fd614c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 14:40:11 +1200 Subject: [PATCH 78/86] :zap: Make text at modules' docs selectable --- src/styl/tags/docs-panel.styl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styl/tags/docs-panel.styl b/src/styl/tags/docs-panel.styl index 561ea92b9..7e93d6806 100644 --- a/src/styl/tags/docs-panel.styl +++ b/src/styl/tags/docs-panel.styl @@ -19,6 +19,7 @@ docs-panel height 100% padding 1rem 2rem 2rem box-sizing border-box + user-select text aside ul From 6585707702a3515477fe1f5994c4e7d64e6eca70 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 14:40:37 +1200 Subject: [PATCH 79/86] :zap: Add a border to navigation panel of the modules' docs --- src/styl/tags/docs-panel.styl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styl/tags/docs-panel.styl b/src/styl/tags/docs-panel.styl index 7e93d6806..f38039dab 100644 --- a/src/styl/tags/docs-panel.styl +++ b/src/styl/tags/docs-panel.styl @@ -9,6 +9,7 @@ docs-panel flex 0 0 auto height 100% line-height 1.75 + border-right 1px solid bd h3 &:first-child margin-top 0 From aabae631e72a99788530616a3b50a31ab9973386 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 14:41:05 +1200 Subject: [PATCH 80/86] :zap: Improvements to numerous modules' docs layour --- app/data/ct.libs/random/DOCS.md | 28 ------------------------- app/data/ct.libs/random/README.md | 22 ++++++++++++++++++- app/data/ct.libs/sound.howler/README.md | 6 ++++-- app/data/ct.libs/transition/README.md | 4 +++- 4 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 app/data/ct.libs/random/DOCS.md diff --git a/app/data/ct.libs/random/DOCS.md b/app/data/ct.libs/random/DOCS.md deleted file mode 100644 index fbeafbdf0..000000000 --- a/app/data/ct.libs/random/DOCS.md +++ /dev/null @@ -1,28 +0,0 @@ -## ct.random(x) - -Returns a random float value between 0 and x, exclusive. - - -## ct.random.dice(dice1,dice2,...diceN) - -Returns a random given argument. - - -## ct.random.range(x1, x2) - -Returns a random float value between `x1` and `x2`, exclusive. - - -## ct.random.deg() - -Returns a random float value between 0 and 360, exclusive. - - -## ct.random.coord() - -Returns a pair of random coordinates from 0 to a corresponding room side. - - -## ct.random.chance(x[, y]) - -When given both `x` and `y`, randomly returns `true` approximately `x` times out of `y`. When given only a value between 0…100, returns `true` approximately `x` times out of 100. E.g. `ct.random.chance(30)` means a 30% success rate. diff --git a/app/data/ct.libs/random/README.md b/app/data/ct.libs/random/README.md index d0f7c9c7e..16c94c049 100644 --- a/app/data/ct.libs/random/README.md +++ b/app/data/ct.libs/random/README.md @@ -1 +1,21 @@ -This module contains handy functions for generating something random. \ No newline at end of file +# ct.random + +This module contains handy functions for generating something random. + +## `ct.random(x)` +Returns a random float value between 0 and x, exclusive. + +## `ct.random.dice(dice1,dice2,...diceN)` +Returns a random given argument. + +## `ct.random.range(x1, x2)` +Returns a random float value between `x1` and `x2`, exclusive. + +## `ct.random.deg()` +Returns a random float value between 0 and 360, exclusive. + +## `ct.random.coord()` +Returns a pair of random coordinates from 0 to a corresponding room side. + +## `ct.random.chance(x[, y])` +When given both `x` and `y`, randomly returns `true` approximately `x` times out of `y`. When given only a value between 0…100, returns `true` approximately `x` times out of 100. E.g. `ct.random.chance(30)` means a 30% success rate. diff --git a/app/data/ct.libs/sound.howler/README.md b/app/data/ct.libs/sound.howler/README.md index c72f8bb12..b2497002e 100644 --- a/app/data/ct.libs/sound.howler/README.md +++ b/app/data/ct.libs/sound.howler/README.md @@ -1,6 +1,8 @@ +# ct.sound.howler + This module replaces the functionality of ct.sound with [Howler.js library](https://github.com/goldfire/howler.js). It creates a more reliable sound system with additional functions like spacial audio, fading, volume changing. -Includes [Howler.js](https://github.com/goldfire/howler.js) v2.1.2. +Includes [Howler.js](https://github.com/goldfire/howler.js) v2.2.0. ## Is it compatible with the default `ct.sound` library? @@ -12,4 +14,4 @@ All the Howl objects are stored in `ct.res.sounds`, like `ct.res.sounds['Puff']` ## What is the difference between "music" and generic audio assets? -All the sounds not marked as `music` are preloaded before the actual game starts. Music can be preloaded manually with a `ct.sound.load('MusicNameHere');` function, or it will just start loading when requested. Also, under the hood, "music" files play in a way that is more effective for large soundtracks. \ No newline at end of file +All the sounds not marked as `music` are preloaded before the actual game starts. Music can be preloaded manually with a `ct.sound.load('MusicNameHere');` function, or it will just start loading when requested. Also, under the hood, "music" files play in a way that is more effective for large soundtracks. diff --git a/app/data/ct.libs/transition/README.md b/app/data/ct.libs/transition/README.md index b88ec1a98..d529a06e7 100644 --- a/app/data/ct.libs/transition/README.md +++ b/app/data/ct.libs/transition/README.md @@ -1,3 +1,5 @@ +# ct.transition + This module allows making transitions between rooms, camera frames, and whenever you want. Transitions are made in a separate UI layer that will overlap everything in your room. This module needs `ct.tween` enabled. It is installed in ct.js by default. @@ -71,4 +73,4 @@ Then, in the `InGame` room: ```js ct.transition.circleIn(500, 0xffd300); // Yellow color, 0.5s to fade in, transitions in a circular shape -``` \ No newline at end of file +``` From 48fe76a435353f13cef434a52770d8dec62cbb04 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:38:25 +1200 Subject: [PATCH 81/86] :zap: Unite doc pages of ct.flow module --- app/data/ct.libs/flow/DOCS.md | 107 -------------------------------- app/data/ct.libs/flow/README.md | 107 +++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 108 deletions(-) delete mode 100644 app/data/ct.libs/flow/DOCS.md diff --git a/app/data/ct.libs/flow/DOCS.md b/app/data/ct.libs/flow/DOCS.md deleted file mode 100644 index 8b4a46d5f..000000000 --- a/app/data/ct.libs/flow/DOCS.md +++ /dev/null @@ -1,107 +0,0 @@ -## ct.flow.gate(func, [opened]) ⇒ `function` - -Constructs and returns a gate — a new function that will execute a given one only when the gate is opened. -The gate can be opened and closed with `ourGate.open()`, `yourGate.close()` and `yourGate.toggle()` methods, -and called later with `yourGate()`. - -**Example:** - -```js -var gate = ct.flow.gate(() => { - console.log('It works!'); -}); -gate(); // Does not work, the gate is not opened by default. -gate.open(); -gate(); // Will now work -``` - -**Returns**: `function` - The constructed gate. - -| Param | Type | Description | -| --- | --- | --- | -| func | `function` | The function to execute. | -| [opened] | `Boolean` | The initial state of the created gate (`true` for being opened, default is `false`). | - - -## ct.flow.delay(func, [opened]) ⇒ `function` - -Creates and returns a new function that will execute a function `func` with a delay -and no more than once in the given period. The call to the original function is made -after the period has elapsed, after which the delay function may be triggered again. - -**Example:** - -```js -var delayed1 = ct.flow.delay(() => { - console.log('It works!'); -}, 300); -for (var i = 0; i < 3; i++) { - delayed1(); // The original function will work only once, because there are no pauses -} -var delayed2 = ct.flow.delay(() => { - console.log('It works!'); -}, 300); -delayed2(); -ct.u.wait(500) -.then(delayed2); // Will work twice -``` - -**Note:** the created function will stop working if the game switches to another room. - -**Returns**: `function` - The delayed function - -| Param | Type | Description | -| --- | --- | --- | -| func | `function` | The function to postpone | -| ms | `Number` | The period to wait, in milliseconds | - - -## ct.flow.retriggerableDelay(func, ms) ⇒ `function` - -Returns a new function that sets a timer to call the original function, -similar to `ct.flow.delay`. This function, however, will reset its timer on each call. -This construction is also known as a cumulative delay. - -**Example:** - -```js -var delayed1 = ct.flow.delay(() => { - console.log('It works!'); -}, 3000); -delayed1(); -ct.u.wait(2000) -.then(delayed2); // Will work once in 5 seconds - -var delayed2 = ct.flow.delay(() => { - console.log('It works!'); -}, 3000); -delayed2(); -ct.u.wait(4000) -.then(delayed2); // Will work twice in 7 seconds -``` - -**Note:** the created function will stop working if the game switches to another room. - -**Returns**: `function` - The delayed function - -| Param | Type | Description | -| --- | --- | --- | -| func | `function` | The function to postpone | -| ms | `Number` | The period to wait, in milliseconds | - - -## ct.flow.timer(func, ms) ⇒ `function` - -Similar to `ct.flow.delay`, this method will return a new function that will limit -the execution of the `func` to max once in `ms` period. It will call the function first -and then block the execution for `ms` time, though. - -It takes into account `ct.delta` or `ct.deltaUi`. - -**Returns**: `function` - a new triggerable function - -| Param | Type | Description | -| --- | --- | --- | -| func | `function` | The function to limit | -| ms | `Number` | The period to wait, in milliseconds | -| [useUiDelta=false] | `Boolean` | If true, use `ct.deltaUi` instead of `ct.delta` | diff --git a/app/data/ct.libs/flow/README.md b/app/data/ct.libs/flow/README.md index e7c673d3a..8db49bbc3 100644 --- a/app/data/ct.libs/flow/README.md +++ b/app/data/ct.libs/flow/README.md @@ -1 +1,106 @@ -A collection of high-level utilities for flow control, that are especially useful while working with asynchronous events. \ No newline at end of file +# ct.flow +`ct.flow` is a collection of high-level utilities for flow control, that are especially useful while working with asynchronous events. + +## ct.flow.gate(func, [opened]) ⇒ `function` +Constructs and returns a gate — a new function that will execute a given one only when the gate is opened. +The gate can be opened and closed with `ourGate.open()`, `yourGate.close()` and `yourGate.toggle()` methods, +and called later with `yourGate()`. + +**Example:** + +```js +var gate = ct.flow.gate(() => { + console.log('It works!'); +}); +gate(); // Does not work, the gate is not opened by default. +gate.open(); +gate(); // Will now work +``` + +**Returns**: `function` - The constructed gate. + +| Param | Type | Description | +| --- | --- | --- | +| func | `function` | The function to execute. | +| [opened] | `Boolean` | The initial state of the created gate (`true` for being opened, default is `false`). | + + +## ct.flow.delay(func, [opened]) ⇒ `function` +Creates and returns a new function that will execute a function `func` with a delay +and no more than once in the given period. The call to the original function is made +after the period has elapsed, after which the delay function may be triggered again. + +**Example:** + +```js +var delayed1 = ct.flow.delay(() => { + console.log('It works!'); +}, 300); +for (var i = 0; i < 3; i++) { + delayed1(); // The original function will work only once, because there are no pauses +} +var delayed2 = ct.flow.delay(() => { + console.log('It works!'); +}, 300); +delayed2(); +ct.u.wait(500) +.then(delayed2); // Will work twice +``` + +**Note:** the created function will stop working if the game switches to another room. + +**Returns**: `function` - The delayed function + +| Param | Type | Description | +| --- | --- | --- | +| func | `function` | The function to postpone | +| ms | `Number` | The period to wait, in milliseconds | + + +## ct.flow.retriggerableDelay(func, ms) ⇒ `function` +Returns a new function that sets a timer to call the original function, +similar to `ct.flow.delay`. This function, however, will reset its timer on each call. +This construction is also known as a cumulative delay. + +**Example:** + +```js +var delayed1 = ct.flow.delay(() => { + console.log('It works!'); +}, 3000); +delayed1(); +ct.u.wait(2000) +.then(delayed2); // Will work once in 5 seconds + +var delayed2 = ct.flow.delay(() => { + console.log('It works!'); +}, 3000); +delayed2(); +ct.u.wait(4000) +.then(delayed2); // Will work twice in 7 seconds +``` + +**Note:** the created function will stop working if the game switches to another room. + +**Returns**: `function` - The delayed function + +| Param | Type | Description | +| --- | --- | --- | +| func | `function` | The function to postpone | +| ms | `Number` | The period to wait, in milliseconds | + + +## ct.flow.timer(func, ms) ⇒ `function` +Similar to `ct.flow.delay`, this method will return a new function that will limit +the execution of the `func` to max once in `ms` period. It will call the function first +and then block the execution for `ms` time, though. + +It takes into account `ct.delta` or `ct.deltaUi`. + +**Returns**: `function` - a new triggerable function + +| Param | Type | Description | +| --- | --- | --- | +| func | `function` | The function to limit | +| ms | `Number` | The period to wait, in milliseconds | +| [useUiDelta=false] | `Boolean` | If true, use `ct.deltaUi` instead of `ct.delta` | From 8d9be4bf859287abaf0758828fdd8d3dcd1d46b4 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:39:02 +1200 Subject: [PATCH 82/86] :zap: Modify keyboard.legacy's name so it is clear that it is outdated --- app/data/ct.libs/keyboard.legacy/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/ct.libs/keyboard.legacy/module.json b/app/data/ct.libs/keyboard.legacy/module.json index 8648aeef3..da667b9d9 100644 --- a/app/data/ct.libs/keyboard.legacy/module.json +++ b/app/data/ct.libs/keyboard.legacy/module.json @@ -1,6 +1,6 @@ { "main": { - "name": "Keyboard", + "name": "Keyboard (legacy)", "tagline": "The old implementation of ct.keyboard that did not support Actions system.", "version": "2.0.0", "authors": [{ From 35d4ba54522937fd14411d8d23a03d057591a5ab Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:42:35 +1200 Subject: [PATCH 83/86] :bento: Fetch the latest docs --- comigojiChangelog.js | 2 +- docs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comigojiChangelog.js b/comigojiChangelog.js index 1d654b845..928b27dd0 100644 --- a/comigojiChangelog.js +++ b/comigojiChangelog.js @@ -54,7 +54,7 @@ module.exports = new Promise((resolve, reject) => { }, assets: { pattern: /^:(briefcase|bento):/, - header: '### 🍱 Demos and Stuff' + header: '### 🍱 Demos, Dependencies and Stuff' }, docs: { pattern: /^:(books|pencil|pencil2|memo):/, diff --git a/docs b/docs index 6bbd4c9d0..aaceeb11a 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 6bbd4c9d05bb6d1a976489882001819ea8c328f7 +Subproject commit aaceeb11a3de92497ac858f4b0326e2d5476f71e From 1f445bc7b22229635903814f6aea92186e295a08 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:43:56 +1200 Subject: [PATCH 84/86] :bug: Rename 1.3.2 migration process to 1.4.0. v1.3.2 does not exist. --- src/js/migration/{1.3.2.js => 1.4.0.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/js/migration/{1.3.2.js => 1.4.0.js} (98%) diff --git a/src/js/migration/1.3.2.js b/src/js/migration/1.4.0.js similarity index 98% rename from src/js/migration/1.3.2.js rename to src/js/migration/1.4.0.js index 4c2952f24..1415426df 100644 --- a/src/js/migration/1.3.2.js +++ b/src/js/migration/1.4.0.js @@ -1,7 +1,7 @@ window.migrationProcess = window.migrationProcess || []; window.migrationProcess.push({ - version: '1.3.2', + version: '1.4.0', process: project => new Promise(resolve => { /** * Project settings got reorganized, logically and visually From d95d70acd32c79f8947471ed930a7d751e2254d8 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:49:34 +1200 Subject: [PATCH 85/86] :pencil: Update Changelog.md --- app/Changelog.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/app/Changelog.md b/app/Changelog.md index 0d7d5845d..adae22be0 100644 --- a/app/Changelog.md +++ b/app/Changelog.md @@ -1,3 +1,80 @@ +## v1.4.0 + +*Sun Aug 09 2020* + +### ✨ New Features + +* Bitmap fonts — see new docs on how to use them. These fonts solve issues with blurry pixelart fonts in games, and also provide higher performance for dynamic text! +* `ct.assert` module for readable checks in ct.js projects +* `ct.camera` now supports direct assignment for its scale, e.g. `ct.camera.scale = 1.5;` +* `ct.inherit` module that allows you to call parents' code and keep things DRY +* Custom font selector in the style editor +* Lucas Dracula theme — A rough port of Arkham theme for VSCode by @lucasmsa +* Modding: `onbeforecreate` injection +* Modding: A `code` input type for monospace text input +* Modding: Add `point2D` input type for modules' settings and injections +* Modding: Add extensions for rooms with `roomExtends` field +* Modding: Add extensions to tile layers with `tileLayerExtends` field +* Modding: Both module settings and asset extensions now can use all the input fields that were previously exclusive to either modules' settings or type extensions +* Module's settings are now parts of the Project Settings' tab +* Quickly create a new type by right-clicking an asset in the textures panel +* Unified module's docs in the side panel + +### ⚡️ General Improvements + +* Allow `ct.fittoscreen` to toggle fullscreen mode while being in an electron app (in a desktop build) + Closes #155 +* Allow games enter fullscreen while being in debugger + See #155 +* Better project selector background for night themes +* Better layout of a type editor +* Change `ct.place.tile` to check against collision groups (new!) instead of depth +* Improve Horizon theme +* Make the structural behavior of TileLayer consistent. Fixes drawing issues with tiles and `ct.place` debug mode +* Minor UI fixes for the project selector +* More logical color hierarchy — you will see subtle changes in how certain panels are colored in dark and light themes, and all themes should now have uniform look and feel +* Move depth input at the type editor into a scrollbox, on par with module-provided fields +* New icons for the top panel +* Refurbish project's settings screen +* Remove empty "help" field from ct.place > module.json +* Replace node-static for dev and docs servers with serve-handler. Solves rare race conditions while loading docs or a game. +* Show a loading icon while exporting project +* The left button group at the topmost tab bar now occupies less space on wider screens +* Update Russian UI translation +* Update Spanish translation for ct.IDE. Update by Stuck Up Creations from the Discord server :sparkles: +* Update debug translation file and comments file +* Use less restrictive YAML reader/writer to allow some minor save file errors + +### 🐛 Bug Fixes + +* Do not reuse tiles directly from room templates + Closes #191 +* Fix blank autocompletion list at room-events-editor + Closes #195 +* Fix `ct.tween.add` not working as expected for useUiDelta + See #198 +* Fix the first tile layer not being added into a drawing stack at room-editor, which made tiles invisible unless a copy or background was added + Closes #206 +* Fix wrong default setting for `ct.fittoscreen` module +* Replace unzipper module and fix issues with module imports + +### 🍱 Demos, dependencies and Stuff + +* Update Howler.js to v2.2.0 + +### 📝 Docs + +* Split "Making catmods" docs into several pages; +* Document the usage of new asset extensions and input types; +* Document the usage of BitmapFonts; +* Update screenshots and directions for tutorials, to reflect UI changes in v1.4. + +### 👾 Misc + +* :fire: Remove export options: HTML and CSS are now always minified, and JS conversion never worked correctly +* :fire: Remove a button in the nav that toggles fullscreen view +* Minor fixes to the debugger files (#197 by @leedigital) + ## v1.3.0 *Wed May 06 2020* @@ -870,4 +947,4 @@ Added license. TL;DR: ### 🍱 Updated demos - Update Catformer demo with fullscreen option -- Update Catsteroids with ct.sound.howler \ No newline at end of file +- Update Catsteroids with ct.sound.howler From 1128e1fc130172ddb3c21d7eefd21314f122020d Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 9 Aug 2020 15:51:04 +1200 Subject: [PATCH 86/86] :bookmark: Bump version to v1.4.0 --- app/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index bb6a503b0..1554138ed 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "main": "index.html", "name": "ctjs", "description": "ct.js — a free 2D game engine", - "version": "1.3.2", + "version": "1.4.0", "homepage": "https://ctjs.rocks/", "author": { "name": "Cosmo Myzrail Gorynych", diff --git a/package.json b/package.json index 95d2803b9..bd6e6b810 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctjsbuildenvironment", - "version": "1.3.1", + "version": "1.4.0", "description": "", "directories": { "doc": "docs"