diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ca15305 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +max_line_length = off diff --git a/.gitignore b/.gitignore index 5b9e087..cd9462c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,5 @@ .venv -coverage -/node_modules -/out/assets -/out/app.* -/out/tests.* -/out/dependencies.js -/graphics/**/*.blend?* -/graphics/**/output.png -/typings/ -*.log -*.tsbuildinfo +node_modules +.rts2_cache_* +.coverage +/dist/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..19b47c0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,11 @@ +image: node:latest + +cache: + paths: + - node_modules/ + +test: + before_script: + - npm install + script: + - npm test diff --git a/activate_node b/activate_node index 9cc5da8..830ac00 100644 --- a/activate_node +++ b/activate_node @@ -3,7 +3,7 @@ # source activate_node vdir="./.venv" -expected="10.15.3" +expected="12.13.0" if [ \! -f "./activate_node" ] then diff --git a/graphics/ships/_base.blend1 b/graphics/ships/_base.blend1 new file mode 100644 index 0000000..2596a55 Binary files /dev/null and b/graphics/ships/_base.blend1 differ diff --git a/graphics/ships/avenger.blend1 b/graphics/ships/avenger.blend1 new file mode 100644 index 0000000..6d45028 Binary files /dev/null and b/graphics/ships/avenger.blend1 differ diff --git a/graphics/ships/whirlwind.blend1 b/graphics/ships/whirlwind.blend1 new file mode 100644 index 0000000..d82c50d Binary files /dev/null and b/graphics/ships/whirlwind.blend1 differ diff --git a/graphics/title.blend1 b/graphics/title.blend1 new file mode 100644 index 0000000..f8f1b99 Binary files /dev/null and b/graphics/title.blend1 differ diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..e528ee6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,27 @@ +module.exports = { + transform: { + "^.+\\.ts$": "ts-jest" + }, + moduleFileExtensions: [ + "ts", + "js", + "json", + "node" + ], + watchPathIgnorePatterns: [ + "/dist/", + "/node_modules/", + ], + restoreMocks: true, + collectCoverage: true, + collectCoverageFrom: [ + "src/**/*.ts", + "!src/**/*.test.ts", + ], + coverageDirectory: ".coverage", + coverageReporters: [ + "lcovonly", + "html", + "text-summary" + ] +} diff --git a/out/aiworker.js b/out/aiworker.js deleted file mode 100644 index 9e96720..0000000 --- a/out/aiworker.js +++ /dev/null @@ -1,23 +0,0 @@ -var handler = { - get(target, name) { - return new Proxy(function () { }, handler); - } -} -var Phaser = new Proxy(function () { }, handler); - -//var debug = console.log; -var debug = function () { }; - -importScripts("app.js"); - -onmessage = function (e) { - debug("[AI Worker] Received", e.data.length); - var serializer = new TK.Serializer(TK.SpaceTac); - var battle = serializer.unserialize(e.data); - var processing = new TK.SpaceTac.AIWorker(battle); - processing.processHere(function (maneuver) { - debug("[AI Worker] Send", maneuver); - postMessage(serializer.serialize(maneuver)); - return maneuver.apply(battle); - }).catch(postMessage); -} diff --git a/out/cursors/button.cur b/out/cursors/button.cur deleted file mode 100644 index 0c9d514..0000000 Binary files a/out/cursors/button.cur and /dev/null differ diff --git a/out/cursors/button.png b/out/cursors/button.png deleted file mode 100644 index 8c234c5..0000000 Binary files a/out/cursors/button.png and /dev/null differ diff --git a/out/cursors/standard.cur b/out/cursors/standard.cur deleted file mode 100644 index 7d8cf53..0000000 Binary files a/out/cursors/standard.cur and /dev/null differ diff --git a/out/cursors/standard.png b/out/cursors/standard.png deleted file mode 100644 index 47f8444..0000000 Binary files a/out/cursors/standard.png and /dev/null differ diff --git a/out/favicon.ico b/out/favicon.ico deleted file mode 100644 index c4efce9..0000000 Binary files a/out/favicon.ico and /dev/null differ diff --git a/out/fonts/control-freak.regular.ttf b/out/fonts/control-freak.regular.ttf deleted file mode 100644 index 4ad47b1..0000000 Binary files a/out/fonts/control-freak.regular.ttf and /dev/null differ diff --git a/out/fonts/daggersquare.regular.otf b/out/fonts/daggersquare.regular.otf deleted file mode 100644 index a0326fa..0000000 Binary files a/out/fonts/daggersquare.regular.otf and /dev/null differ diff --git a/out/fonts/fragile-bombers.regular.ttf b/out/fonts/fragile-bombers.regular.ttf deleted file mode 100644 index 28703e9..0000000 Binary files a/out/fonts/fragile-bombers.regular.ttf and /dev/null differ diff --git a/out/fonts/mysterons-brk.normal.ttf b/out/fonts/mysterons-brk.normal.ttf deleted file mode 100644 index ee626e5..0000000 Binary files a/out/fonts/mysterons-brk.normal.ttf and /dev/null differ diff --git a/out/fonts/planetium-x.regular.otf b/out/fonts/planetium-x.regular.otf deleted file mode 100644 index bfcec49..0000000 Binary files a/out/fonts/planetium-x.regular.otf and /dev/null differ diff --git a/out/images/preload/bar-background.png b/out/images/preload/bar-background.png deleted file mode 100644 index 7379627..0000000 Binary files a/out/images/preload/bar-background.png and /dev/null differ diff --git a/out/images/preload/bar-content.png b/out/images/preload/bar-content.png deleted file mode 100644 index 130d45c..0000000 Binary files a/out/images/preload/bar-content.png and /dev/null differ diff --git a/out/index.html b/out/index.html deleted file mode 100644 index 3993e1f..0000000 --- a/out/index.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - SpaceTac - - - - - -
-
.
- - - - - - - - \ No newline at end of file diff --git a/out/play.png b/out/play.png deleted file mode 100644 index f509241..0000000 Binary files a/out/play.png and /dev/null differ diff --git a/out/sim.js b/out/sim.js deleted file mode 100644 index 90454a9..0000000 --- a/out/sim.js +++ /dev/null @@ -1,68 +0,0 @@ -const Pool = require('process-pool').default; - -const pool = new Pool({ processLimit: 8 }); -const work = pool.prepare(function () { - const App = require("./app").TK.SpaceTac; - - async function doOneBattle(i) { - let ai1 = new App.TacticalAI(); - let ai2 = new App.TacticalAI(); - - // Prepare battle - let battle = App.Battle.newQuickRandom(true, 1, 2 + i % 4); - battle.fleets.forEach((fleet, findex) => { - fleet.ships.forEach((ship, sindex) => { - ship.name = `F${findex + 1}S${sindex + 1} (${ship.model.name})`; - }); - }); - - // Run battle - while (!battle.ended && battle.cycle < 100) { - let playing = battle.playing_ship; - if (playing) { - let ai = (playing.fleet == battle.fleets[0]) ? ai1 : ai2; - ai.ship = playing; - await ai.play(); - } - } - - // Collect results - if (battle.outcome && battle.outcome.winner) { - let results = {}; - battle.fleets.forEach(fleet => { - fleet.ships.forEach(ship => { - let name = `Level ${ship.level.get()} ${ship.model.name}`; - results[name] = (results[name] || 0) + (ship.fleet === battle.outcome.winner ? 1 : 0); - }); - }); - return results; - } else { - return {}; - } - } - - return (i) => doOneBattle(i); -}); - -let played = {}; -let winned = {}; -let works = Array.from({ length: 1000 }, (v, i) => i).map(i => { - return work(i).then(result => { - Object.keys(result).forEach(model => { - if (result[model]) { - winned[model] = (winned[model] || 0) + 1; - } - played[model] = (played[model] || 0) + 1; - }); - - console.warn("------------------------------------------------"); - console.warn(`--- Results after battle ${i}`); - Object.keys(played).sort().forEach(model => { - let factor = (winned[model] || 0) / played[model]; - console.warn(`${model} ${Math.round(factor * 100)}%`); - }); - console.warn("------------------------------------------------"); - }); -}); - -Promise.all(works).then(() => process.exit(0)); diff --git a/out/tests.html b/out/tests.html deleted file mode 100644 index 8e78454..0000000 --- a/out/tests.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - SpaceTac - Unit tests - - - - - - -
-
-
- - - - - - - - - - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7b6e01c..dc7acbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,30 +4,1291 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@babel/parser": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", - "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==" - }, - "@babel/runtime": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", - "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, "requires": { - "regenerator-runtime": "^0.13.2" + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.2.tgz", + "integrity": "sha512-eeD7VEZKfhK1KUXGiyPFettgF3m513f8FoBSWiQ1xTvl1RAopLs42Wp9+Ze911I6H0N9lNqJMDgoZT7gHsipeQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.2", + "@babel/helpers": "^7.7.0", + "@babel/parser": "^7.7.2", + "@babel/template": "^7.7.0", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.7.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" }, "dependencies": { - "regenerator-runtime": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", - "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, + "@babel/generator": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.2.tgz", + "integrity": "sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.0.tgz", + "integrity": "sha512-k50CQxMlYTYo+GGyUGFwpxKVtxVJi9yh61sXZji3zYHccK9RYliZGSTOgci85T+r+0VFN2nWbGM04PIqwfrpMg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.7.0.tgz", + "integrity": "sha512-Cd8r8zs4RKDwMG/92lpZcnn5WPQ3LAMQbCw42oqUh4s7vsSN5ANUZjMel0OOnxDLq57hoDDbai+ryygYfCTOsw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.7.0.tgz", + "integrity": "sha512-LSln3cexwInTMYYoFeVLKnYPPMfWNJ8PubTBs3hkh7wCu9iBaqq1OOyW+xGmEdLxT1nhsl+9SJ+h2oUDYz0l2A==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.7.0.tgz", + "integrity": "sha512-Su0Mdq7uSSWGZayGMMQ+z6lnL00mMCnGAbO/R0ZO9odIdB/WNU/VfQKqMQU0fdIsxQYbRjDM4BixIa93SQIpvw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.0.tgz", + "integrity": "sha512-MZiB5qvTWoyiFOgootmRSDV1udjIqJW/8lmxgzKq6oDqxdmHUjeP2ZUOmgHdYjmUVNABqRrHjYAYRvj8Eox/UA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.0", + "@babel/helper-member-expression-to-functions": "^7.7.0", + "@babel/helper-optimise-call-expression": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.0", + "@babel/helper-split-export-declaration": "^7.7.0" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.2.tgz", + "integrity": "sha512-pAil/ZixjTlrzNpjx+l/C/wJk002Wo7XbbZ8oujH/AoJ3Juv0iN/UTcPUHXKMFLqsfS0Hy6Aow8M31brUYBlQQ==", + "dev": true, + "requires": { + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.6.0" + } + }, + "@babel/helper-define-map": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.7.0.tgz", + "integrity": "sha512-kPKWPb0dMpZi+ov1hJiwse9dWweZsz3V9rP4KdytnX1E7z3cTNmFGglwklzFPuqIcHLIY3bgKSs4vkwXXdflQA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.0", + "@babel/types": "^7.7.0", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.7.0.tgz", + "integrity": "sha512-CDs26w2shdD1urNUAji2RJXyBFCaR+iBEGnFz3l7maizMkQe3saVw9WtjG1tz8CwbjvlFnaSLVhgnu1SWaherg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-function-name": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz", + "integrity": "sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.0", + "@babel/template": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz", + "integrity": "sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.0.tgz", + "integrity": "sha512-LUe/92NqsDAkJjjCEWkNe+/PcpnisvnqdlRe19FahVapa4jndeuJ+FBiTX1rcAKWKcJGE+C3Q3tuEuxkSmCEiQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.0.tgz", + "integrity": "sha512-QaCZLO2RtBcmvO/ekOLp8p7R5X2JriKRizeDpm5ChATAFWrrYDcDxPuCIBXKyBjY+i1vYSdcUTMIb8psfxHDPA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.0.tgz", + "integrity": "sha512-Dv3hLKIC1jyfTkClvyEkYP2OlkzNvWs5+Q8WgPbxM5LMeorons7iPP91JM+DU7tRbhqA1ZeooPaMFvQrn23RHw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.0.tgz", + "integrity": "sha512-rXEefBuheUYQyX4WjV19tuknrJFwyKw0HgzRwbkyTbB+Dshlq7eqkWbyjzToLrMZk/5wKVKdWFluiAsVkHXvuQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.0", + "@babel/helper-simple-access": "^7.7.0", + "@babel/helper-split-export-declaration": "^7.7.0", + "@babel/template": "^7.7.0", + "@babel/types": "^7.7.0", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.0.tgz", + "integrity": "sha512-48TeqmbazjNU/65niiiJIJRc5JozB8acui1OS7bSd6PgxfuovWsvjfWSzlgx+gPFdVveNzUdpdIg5l56Pl5jqg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", + "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "dev": true, + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.7.0.tgz", + "integrity": "sha512-pHx7RN8X0UNHPB/fnuDnRXVZ316ZigkO8y8D835JlZ2SSdFKb6yH9MIYRU4fy/KPe5sPHDFOPvf8QLdbAGGiyw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.0", + "@babel/helper-wrap-function": "^7.7.0", + "@babel/template": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.0.tgz", + "integrity": "sha512-5ALYEul5V8xNdxEeWvRsBzLMxQksT7MaStpxjJf9KsnLxpAKBtfw5NeMKZJSYDa0lKdOcy0g+JT/f5mPSulUgg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.7.0", + "@babel/helper-optimise-call-expression": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.7.0.tgz", + "integrity": "sha512-AJ7IZD7Eem3zZRuj5JtzFAptBw7pMlS3y8Qv09vaBWoFsle0d1kAn5Wq6Q9MyBXITPOKnxwkZKoAm4bopmv26g==", + "dev": true, + "requires": { + "@babel/template": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz", + "integrity": "sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.0" + } + }, + "@babel/helper-wrap-function": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.7.0.tgz", + "integrity": "sha512-sd4QjeMgQqzshSjecZjOp8uKfUtnpmCyQhKQrVJBBgeHAB/0FPi33h3AbVlVp07qQtMD4QgYSzaMI7VwncNK/w==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.0", + "@babel/template": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/helpers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.0.tgz", + "integrity": "sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g==", + "dev": true, + "requires": { + "@babel/template": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.3.tgz", + "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.0.tgz", + "integrity": "sha512-ot/EZVvf3mXtZq0Pd0+tSOfGWMizqmOohXmNZg6LNFjHOV+wOPv7BvVYh8oPR8LhpIP3ye8nNooKL50YRWxpYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz", + "integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.5.5", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.0.tgz", + "integrity": "sha512-7poL3Xi+QFPC7sGAzEIbXUyYzGJwbc2+gSD0AkiC5k52kH2cqHdqxm5hNFfLW3cRSTcx9bN0Fl7/6zWcLLnKAQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz", + "integrity": "sha512-LDBXlmADCsMZV1Y9OQwMc0MyGZ8Ta/zlD9N67BfQT8uYwkRswiu2hU6nJKrjrt/58aH/vqfQlR/9yId/7A2gWw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.0.tgz", + "integrity": "sha512-mk34H+hp7kRBWJOOAR0ZMGCydgKMD4iN9TpDRp3IIcbunltxEY89XSimc6WbtSLCDrwcdy/EEw7h5CFCzxTchw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.7.0.tgz", + "integrity": "sha512-vQMV07p+L+jZeUnvX3pEJ9EiXGCjB5CTTvsirFD9rpEuATnoAvLBLoYbw1v5tyn3d2XxSuvEKi8cV3KqYUa0vQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", + "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.0.tgz", + "integrity": "sha512-hi8FUNiFIY1fnUI2n1ViB1DR0R4QeK4iHcTlW6aJkrPoTdb8Rf1EMQ6GT3f67DDkYyWgew9DFoOZ6gOoEsdzTA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.7.0.tgz", + "integrity": "sha512-vLI2EFLVvRBL3d8roAMqtVY0Bm9C1QzLkdS57hiKrjUBSqsQYrBsMCeOg/0KK7B0eK9V71J5mWcha9yyoI2tZw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz", + "integrity": "sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.7.0.tgz", + "integrity": "sha512-/b3cKIZwGeUesZheU9jNYcwrEA7f/Bo4IdPmvp7oHgvks2majB5BoT5byAql44fiNQYOPzhk2w8DbgfuafkMoA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.0", + "@babel/helper-define-map": "^7.7.0", + "@babel/helper-function-name": "^7.7.0", + "@babel/helper-optimise-call-expression": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.0", + "@babel/helper-split-export-declaration": "^7.7.0", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz", + "integrity": "sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.7.0.tgz", + "integrity": "sha512-3QQlF7hSBnSuM1hQ0pS3pmAbWLax/uGNCbPBND9y+oJ4Y776jsyujG2k0Sn2Aj2a0QwVOiOFL5QVPA7spjvzSA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", + "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.6.3.tgz", + "integrity": "sha512-l0ETkyEofkqFJ9LS6HChNIKtVJw2ylKbhYMlJ5C6df+ldxxaLIyXY4yOdDQQspfFpV8/vDiaWoJlvflstlYNxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.2.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.7.0.tgz", + "integrity": "sha512-P5HKu0d9+CzZxP5jcrWdpe7ZlFDe24bmqP6a6X8BHEBl/eizAsY8K6LX8LASZL0Jxdjm5eEfzp+FIrxCm/p8bA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", + "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.0.tgz", + "integrity": "sha512-KEMyWNNWnjOom8vR/1+d+Ocz/mILZG/eyHHO06OuBQ2aNhxT62fr4y6fGOplRx+CxCSp3IFwesL8WdINfY/3kg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.7.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.7.0.tgz", + "integrity": "sha512-ZAuFgYjJzDNv77AjXRqzQGlQl4HdUM6j296ee4fwKVZfhDR9LAGxfvXjBkb06gNETPnN0sLqRm9Gxg4wZH6dXg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.7.0.tgz", + "integrity": "sha512-u7eBA03zmUswQ9LQ7Qw0/ieC1pcAkbp5OQatbWUzY1PaBccvuJXUkYzoN1g7cqp7dbTu6Dp9bXyalBvD04AANA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.0.tgz", + "integrity": "sha512-+SicSJoKouPctL+j1pqktRVCgy+xAch1hWWTMy13j0IflnyNjaoskj+DwRQFimHbLqO3sq2oN2CXMvXq3Bgapg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.0" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", + "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.7.0.tgz", + "integrity": "sha512-mXhBtyVB1Ujfy+0L6934jeJcSXj/VCg6whZzEcgiiZHNS0PGC7vUCsZDQCxxztkpIdF+dY1fUMcjAgEOC3ZOMQ==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.0.tgz", + "integrity": "sha512-AXmvnC+0wuj/cFkkS/HFHIojxH3ffSXE+ttulrqWjZZRaUOonfJc60e1wSNT4rV8tIunvu/R3wCp71/tLAa9xg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.6.2.tgz", + "integrity": "sha512-DpSvPFryKdK1x+EDJYCy28nmAaIMdxmhot62jAXF/o99iA33Zj2Lmcp3vDmz+MUh0LNYVPvfj5iC3feb3/+PFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.7.0.tgz", + "integrity": "sha512-RrThb0gdrNwFAqEAAx9OWgtx6ICK69x7i9tCnMdVrxQwSDp/Abu9DXFU5Hh16VP33Rmxh04+NGW28NsIkFvFKA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/preset-env": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.7.1.tgz", + "integrity": "sha512-/93SWhi3PxcVTDpSqC+Dp4YxUu3qZ4m7I76k0w73wYfn7bGVuRIO4QUz95aJksbS+AD1/mT1Ie7rbkT0wSplaA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.7.0", + "@babel/plugin-proposal-dynamic-import": "^7.7.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.7.0", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-syntax-top-level-await": "^7.7.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.7.0", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.6.3", + "@babel/plugin-transform-classes": "^7.7.0", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.6.0", + "@babel/plugin-transform-dotall-regex": "^7.7.0", + "@babel/plugin-transform-duplicate-keys": "^7.5.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.7.0", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.5.0", + "@babel/plugin-transform-modules-commonjs": "^7.7.0", + "@babel/plugin-transform-modules-systemjs": "^7.7.0", + "@babel/plugin-transform-modules-umd": "^7.7.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.7.0", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.5.5", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.7.0", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.6.2", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.7.0", + "@babel/types": "^7.7.1", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/preset-flow": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.0.0.tgz", + "integrity": "sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-flow-strip-types": "^7.0.0" + } + }, + "@babel/runtime": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", + "integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/runtime-corejs3": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.2.tgz", + "integrity": "sha512-odQQZpujq0AHttKrvp4n2KGjK5b5cuq7LeEcsdadwZOemMkmJnlgTXMCf5fIixLLaBxUypwn0krKK51vVMA5cg==", + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/template": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.0.tgz", + "integrity": "sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/types": "^7.7.0" + } + }, + "@babel/traverse": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.2.tgz", + "integrity": "sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.2", + "@babel/helper-function-name": "^7.7.0", + "@babel/helper-split-export-declaration": "^7.7.0", + "@babel/parser": "^7.7.2", + "@babel/types": "^7.7.2", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "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==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz", + "integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@cnakazawa/watch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", + "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "requires": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + } + }, + "@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + } + }, + "@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + } + }, + "@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/babel__core": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", + "integrity": "sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.0.tgz", + "integrity": "sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.8.tgz", + "integrity": "sha512-yGeB2dHEdvxjP0y4UbRtQaSkXJ9649fYCmIdRoul5kfAoGCwxuCbMhag0k3RPfnuh9kPGm8x89btcfDEXdVWGw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, "@types/jasmine": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz", - "integrity": "sha512-lXvr2xFQEVQLkIhuGaR3GC1L9lMU1IxeWnAF/wNY5ZWpC4p9dgxkKkzMp7pntpAdv9pZSnYqgsBkCg32MXSZMg==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.6.tgz", + "integrity": "sha512-hpQHs+lmZ0uuCrGyqypdI1Ho7jRFolOBT6OkNdZPFziLSSEKvWu+VxWU6bGdNEA/hoV4jV8pdDeNx8EWlmfNAw==", + "dev": true + }, + "@types/jest": { + "version": "24.0.23", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.23.tgz", + "integrity": "sha512-L7MBvwfNpe7yVPTXLn32df/EK+AMBFAFvZrRuArGs7npEWnlziUXK+5GMIUTI4NIuwok3XibsjXCs5HxviYXjg==", + "dev": true, + "requires": { + "jest-diff": "^24.3.0" + } + }, + "@types/node": { + "version": "12.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.11.tgz", + "integrity": "sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ==", + "dev": true + }, + "@types/parse": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/parse/-/parse-2.9.0.tgz", + "integrity": "sha512-oEun9c/R/5L8+IWttPrZDiomiVghgEuhL5cnkYPpT2Yp9D/t0n/aPTiCwCNR25kRtRDzTE1aKLZZOI+RGyCYUQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/yargs": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", + "integrity": "sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz", + "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==", + "dev": true + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", "dev": true }, "abbrev": { @@ -44,54 +1305,79 @@ "requires": { "mime-types": "~2.1.24", "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" }, "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } } } }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", "dev": true }, - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "dev": true, "requires": { - "es6-promisify": "^5.0.0" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -102,12 +1388,6 @@ "color-convert": "^1.9.0" } }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -133,20 +1413,21 @@ "integrity": "sha1-7klza2ObTxCLbp5ibG2pkwa0FpI=", "dev": true }, + "arg": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", + "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -171,6 +1452,12 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -195,12 +1482,33 @@ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", "dev": true }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", @@ -208,14 +1516,14 @@ "dev": true }, "async-done": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.1.tgz", - "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.2", - "process-nextick-args": "^1.0.7", + "process-nextick-args": "^2.0.0", "stream-exhaust": "^1.0.1" } }, @@ -226,9 +1534,21 @@ "dev": true }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "asyncro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/asyncro/-/asyncro-3.0.0.tgz", + "integrity": "sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg==", + "dev": true }, "atob": { "version": "2.1.2", @@ -236,6 +1556,181 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "autoprefixer": { + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.2.tgz", + "integrity": "sha512-LCAfcdej1182uVvPOZnytbq61AhnOZ/4JelDaJGDeNwewyU1AMaNthcHsyz1NRjTmd2FkurMckLWfkHg3Z//KA==", + "dev": true, + "requires": { + "browserslist": "^4.7.3", + "caniuse-lite": "^1.0.30001010", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.23", + "postcss-value-parser": "^4.0.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "requires": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-plugin-macros": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.7.0.tgz", + "integrity": "sha512-eV/9IWjmwT/TDCrNzA9sgO/j+x1dWAdLds7KLTY/emkLimzYbiXugfa0EO9IMkLeBBYvS0OdY+6pkF5VGf0iog==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "babel-plugin-transform-async-to-promises": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-promises/-/babel-plugin-transform-async-to-promises-0.8.15.tgz", + "integrity": "sha512-fDXP68ZqcinZO2WCiimCL9zhGjGXOnn3D33zvbh+yheZ/qOrNVVDDIBtAaM3Faz8TRvQzHiRKsu3hfrBAhEncQ==", + "dev": true + }, + "babel-plugin-transform-replace-expressions": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-replace-expressions/-/babel-plugin-transform-replace-expressions-0.2.0.tgz", + "integrity": "sha512-Eh1rRd9hWEYgkgoA3D0kGp7xJ/wgVshgsqmq60iC4HVWD+Lux+fNHSHBa2v1Hsv+dHflShC71qKhiH40OiPtDA==", + "dev": true, + "requires": { + "@babel/parser": "^7.3.3" + } + }, + "babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + } + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -330,6 +1825,15 @@ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -363,9 +1867,9 @@ "dev": true }, "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", + "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", "dev": true }, "body-parser": { @@ -384,16 +1888,14 @@ "qs": "6.7.0", "raw-body": "2.4.0", "type-is": "~1.6.17" - }, - "dependencies": { - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -433,6 +1935,67 @@ } } }, + "brotli-size": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", + "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserslist": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.3.tgz", + "integrity": "sha512-jWvmhqYpx+9EZm/FxcZSbUZyDEvDTLDi3nSAKbzEkyWvtI0mNSmUosey+5awDW1RUlrgXbQb5A6qY1xQH9U6MQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001010", + "electron-to-chromium": "^1.3.306", + "node-releases": "^1.1.40" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -462,9 +2025,9 @@ "dev": true }, "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", "dev": true }, "bytes": { @@ -490,12 +2053,44 @@ "unset-value": "^1.0.0" } }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, "callsite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", "dev": true }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", @@ -512,21 +2107,39 @@ "map-obj": "^1.0.0" } }, - "catharsis": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.10.tgz", - "integrity": "sha512-l2OUaz/3PU3MZylspVFJvwHCVfWyvcduPq4lv3AzZ2pJzZCo7kNKFNyatwujD7XgvGkNAE/Jhhbh2uARNwNkfw==", + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, "requires": { - "lodash": "^4.17.11" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - } + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" } }, + "caniuse-lite": { + "version": "1.0.30001011", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001011.tgz", + "integrity": "sha512-h+Eqyn/YA6o6ZTqpS86PyRmNWOs1r54EBDcd2NTwwfsXQ8re1B38SnB+p2RKF8OUsyEIjeDU8XGec1RGO/wYCg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, "chalk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", @@ -536,12 +2149,29 @@ "ansi-styles": "^3.1.0", "escape-string-regexp": "^1.0.5", "supports-color": "^4.0.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } } }, "chokidar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", - "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -566,6 +2196,12 @@ } } }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -589,17 +2225,60 @@ } } }, - "codecov": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.4.0.tgz", - "integrity": "sha512-+vtyL1B11MWiRIBaPnsIALKKpLFck9m6QdyI20ZnG8WqLG2cxwCTW9x/LbG4Ht8b81equZWw5xLcr+0BIvmdJQ==", + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "argv": "^0.0.2", - "ignore-walk": "^3.0.1", - "js-yaml": "^3.13.0", - "teeny-request": "^3.11.3", - "urlgrey": "^0.4.4" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "collection-visit": { @@ -612,6 +2291,16 @@ "object-visit": "^1.0.0" } }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -627,16 +2316,35 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "component-bind": { @@ -663,15 +2371,32 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, "requires": { "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, @@ -681,6 +2406,15 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", "dev": true }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -693,11 +2427,28 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", - "dev": true + "core-js-compat": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.4.2.tgz", + "integrity": "sha512-W0Aj+LM3EAxxjD0Kp2o4be8UlnxIZHNupBv2znqrheR4aY2nOn91794k/xoSp+SxqqriiZpTsSwBtZr60cbkwQ==", + "dev": true, + "requires": { + "browserslist": "^4.7.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.4.2.tgz", + "integrity": "sha512-6+iSif/3zO0bSkhjVY9o4MTdv36X+rO6rqs/UxQ+uxBevmC4fsfwyQwFVdZXXONmLlKVLiXCG8PDvQ2Gn/iteA==" }, "core-util-is": { "version": "1.0.2", @@ -715,6 +2466,383 @@ "vary": "^1" } }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "dependencies": { + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "dev": true, + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + }, + "dependencies": { + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-what": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", + "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==", + "dev": true + }, + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.2.tgz", + "integrity": "sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.37" + } + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -730,10 +2858,43 @@ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", "dev": true }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, "date-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", - "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", "dev": true }, "dateformat": { @@ -773,6 +2934,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -814,6 +2984,12 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -826,12 +3002,30 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", "dev": true }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true + }, "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -844,10 +3038,57 @@ "void-elements": "^2.0.0" } }, - "dts-dom": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/dts-dom/-/dts-dom-3.2.0.tgz", - "integrity": "sha512-M3PfyWqmL/5gEvz8PoogzOhk8UA/WaSUt2B0qS4PdZRMTZnjxlvSqh9AVNsZJflvPK8qjjPb0m1d/alGiYlk3w==" + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } }, "duplexer": { "version": "0.1.1", @@ -855,12 +3096,34 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "electron-to-chromium": { + "version": "1.3.310", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.310.tgz", + "integrity": "sha512-ixvxy46JrDv5c8k1+th66Z+xDZD8zShNs6oh7hgyMpNZUgaoRBisXgFZKAyyhQTAj7oU2Y/uZ0AAsj/TY4N0tA==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", @@ -873,9 +3136,9 @@ "dev": true }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "requires": { "once": "^1.4.0" @@ -984,34 +3247,55 @@ "dev": true }, "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "dev": true }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { "is-arrayish": "^0.2.1" } }, - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "es-abstract": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", "dev": true, "requires": { - "es6-promise": "^4.0.3" + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" } }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promisify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.0.2.tgz", + "integrity": "sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1024,16 +3308,53 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "etag": { @@ -1044,7 +3365,7 @@ }, "event-stream": { "version": "3.3.4", - "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "dev": true, "requires": { @@ -1062,6 +3383,33 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1097,6 +3445,20 @@ } } }, + "expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + } + }, "exports-loader": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.7.0.tgz", @@ -1104,19 +3466,12 @@ "requires": { "loader-utils": "^1.1.0", "source-map": "0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", - "integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=" - } } }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, "extend-shallow": { @@ -1205,21 +3560,70 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", "dev": true, "requires": { "websocket-driver": ">=0.5.1" } }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "^2.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "filesize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", + "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1244,26 +3648,18 @@ } }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.1", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", - "dev": true - } } }, "find-up": { @@ -1277,18 +3673,18 @@ } }, "flatted": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", - "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, "follow-redirects": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", - "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", + "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==", "dev": true, "requires": { - "debug": "^3.2.6" + "debug": "^3.0.0" }, "dependencies": { "debug": { @@ -1301,9 +3697,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -1323,6 +3719,23 @@ "for-in": "^1.0.1" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -1353,6 +3766,17 @@ "null-check": "^1.0.0" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1379,7 +3803,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1400,12 +3825,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1420,17 +3847,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1547,7 +3977,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1559,6 +3990,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1573,6 +4005,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1580,12 +4013,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1604,6 +4039,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -1684,7 +4120,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -1696,6 +4133,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -1781,7 +4219,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -1817,6 +4256,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -1836,6 +4276,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1879,15 +4320,23 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "gamefroot-texture-packer": { "version": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff", "from": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff", @@ -1914,22 +4363,81 @@ } } }, + "generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha1-LXhqEhruUIh2eWk56OO/+DbCCRc=", + "dev": true, + "requires": { + "loader-utils": "^0.2.16" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1975,15 +4483,58 @@ "object.defaults": "^1.1.0" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalyzer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz", + "integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==", + "dev": true + }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -2000,6 +4551,48 @@ } } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", @@ -2029,6 +4622,12 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2061,12 +4660,45 @@ } } }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", "dev": true }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "http-auth": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", @@ -2093,49 +4725,41 @@ } }, "http-parser-js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz", - "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", "dev": true }, "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", + "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", "dev": true, "requires": { - "eventemitter3": "^3.0.0", + "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "dev": true, - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" }, "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "eventemitter3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", "dev": true } } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2145,13 +4769,56 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", "dev": true, "requires": { - "minimatch": "^3.0.4" + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" } }, "imports-loader": { @@ -2170,6 +4837,12 @@ } } }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -2179,6 +4852,12 @@ "repeating": "^2.0.0" } }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -2206,6 +4885,21 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "dev": true }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -2247,13 +4941,33 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", "dev": true, "requires": { - "builtin-modules": "^1.0.0" + "ci-info": "^2.0.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" } }, "is-data-descriptor": { @@ -2276,6 +4990,12 @@ } } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -2295,6 +5015,12 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -2316,6 +5042,18 @@ "number-is-nan": "^1.0.0" } }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -2325,6 +5063,12 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -2351,6 +5095,12 @@ } } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -2360,6 +5110,60 @@ "isobject": "^3.0.1" } }, + "is-reference": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz", + "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==", + "dev": true, + "requires": { + "@types/estree": "0.0.39" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -2405,6 +5209,12 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, "istanbul": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", @@ -2427,31 +5237,6 @@ "wordwrap": "^1.0.0" }, "dependencies": { - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, "glob": { "version": "5.0.15", "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", @@ -2465,24 +5250,11 @@ "path-is-absolute": "^1.0.0" } }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true }, "wordwrap": { "version": "1.0.0", @@ -2492,20 +5264,618 @@ } } }, - "jasmine": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", - "integrity": "sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ==", + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", "dev": true, "requires": { - "glob": "^7.1.3", - "jasmine-core": "~3.4.0" + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "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==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "jasmine": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", + "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "jasmine-core": "~3.5.0" } }, "jasmine-core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz", - "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + }, + "jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "requires": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "dependencies": { + "jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "requires": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + } + } + } + }, + "jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + } + }, + "jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "requires": { + "detect-newline": "^2.1.0" + } + }, + "jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + } + }, + "jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + } + }, + "jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "requires": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true + }, + "jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + } + }, + "jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true + }, + "jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + } + }, + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { @@ -2516,70 +5886,156 @@ "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" - } - }, - "js2xmlparser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.0.tgz", - "integrity": "sha512-WuNgdZOXVmBk5kUPMcTcVUpbGRzLfNkv7+7APq7WiDihpXVKrgxo6wwRpRl9OQeEBgKCVk9mR7RbzrnNWC8oBw==", - "requires": { - "xmlcreate": "^2.0.0" - } - }, - "jsdoc": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.2.tgz", - "integrity": "sha512-S2vzg99C5+gb7FWlrK4TVdyzVPGGkdvpDkCEJH1JABi2PKzPeLu5/zZffcJUifgWUJqXWl41Hoc+MmuM2GukIg==", - "requires": { - "@babel/parser": "^7.4.4", - "bluebird": "^3.5.4", - "catharsis": "^0.8.10", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.0", - "klaw": "^3.0.0", - "markdown-it": "^8.4.2", - "markdown-it-anchor": "^5.0.2", - "marked": "^0.6.2", - "mkdirp": "^0.5.1", - "requizzle": "^0.2.2", - "strip-json-comments": "^3.0.1", - "taffydb": "2.6.2", - "underscore": "~1.9.1" }, "dependencies": { - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" - }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" - }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "escodegen": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", + "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "dev": true, "requires": { - "graceful-fs": "^4.1.9" + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" } } } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" } }, "just-debounce": { @@ -2589,18 +6045,17 @@ "dev": true }, "karma": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.1.0.tgz", - "integrity": "sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz", + "integrity": "sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A==", "dev": true, "requires": { "bluebird": "^3.3.0", "body-parser": "^1.16.1", - "braces": "^2.3.2", - "chokidar": "^2.0.3", + "braces": "^3.0.2", + "chokidar": "^3.0.0", "colors": "^1.1.0", "connect": "^3.6.0", - "core-js": "^2.2.0", "di": "^0.0.1", "dom-serialize": "^2.2.0", "flatted": "^2.0.0", @@ -2608,7 +6063,7 @@ "graceful-fs": "^4.1.2", "http-proxy": "^1.13.0", "isbinaryfile": "^3.0.0", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "log4js": "^4.0.0", "mime": "^2.3.1", "minimatch": "^3.0.2", @@ -2623,23 +6078,116 @@ "useragent": "2.3.0" }, "dependencies": { - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "dev": true }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, @@ -2666,12 +6214,6 @@ "source-map": "^0.5.1" }, "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -2704,6 +6246,24 @@ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -2714,13 +6274,11 @@ "type-check": "~0.3.2" } }, - "linkify-it": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", - "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", - "requires": { - "uc.micro": "^1.0.1" - } + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true }, "live-server": { "version": "1.2.1", @@ -2729,7 +6287,7 @@ "dev": true, "requires": { "chokidar": "^2.0.4", - "colors": "^1.3.3", + "colors": "^1.4.0", "connect": "^3.6.6", "cors": "^2.8.5", "event-stream": "3.3.4", @@ -2739,14 +6297,14 @@ "object-assign": "^4.1.1", "opn": "^6.0.0", "proxy-middleware": "^0.15.0", - "send": "^0.17.0", + "send": "^0.17.1", "serve-index": "^1.9.1" }, "dependencies": { "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, "object-assign": { @@ -2780,10 +6338,46 @@ "json5": "^1.0.1" } }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "lodash.padend": { @@ -2792,17 +6386,29 @@ "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, "log4js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.1.1.tgz", - "integrity": "sha512-tSQUF9bBMdcBtuLD6vD7hBM9Ci6Lng/NVHZEq4YbuRGo7ObmLiZuhxz33HKAmJItit74pAjvZgirqYX2LRaoGA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", "dev": true, "requires": { "date-format": "^2.0.0", "debug": "^4.1.1", "flatted": "^2.0.0", - "rfdc": "^1.1.2", - "streamroller": "^1.0.4" + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" }, "dependencies": { "debug": { @@ -2815,13 +6421,22 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -2842,6 +6457,48 @@ "yallist": "^2.1.2" } }, + "magic-string": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", + "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -2869,32 +6526,83 @@ "object-visit": "^1.0.0" } }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "maxmin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", + "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", + "dev": true, "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "chalk": "^1.0.0", + "figures": "^1.0.1", + "gzip-size": "^3.0.0", + "pretty-bytes": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "dev": true, + "requires": { + "duplexer": "^0.1.1" + } + }, + "pretty-bytes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", + "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, - "markdown-it-anchor": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz", - "integrity": "sha512-AFM/woBI8QDJMS/9+MmsBMT5/AR+ImfOsunQZTZhzcTmna3rIzAzbOh5E0l6mlFM/i9666BpUtkqQ9bS7WApCg==" - }, - "marked": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.2.tgz", - "integrity": "sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==" - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true }, "media-typer": { "version": "0.3.0", @@ -2902,6 +6610,12 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -2918,22 +6632,73 @@ "read-pkg-up": "^1.0.1", "redent": "^1.0.0", "trim-newlines": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "microargs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/microargs/-/microargs-1.1.2.tgz", "integrity": "sha512-fUrX9ozzzUX6JlDoNXmUM5i4B0uBF5xPznZ0Y/izM9wOtAaTf44V2vUCRgGBLUq/SeGIcDgfnEZDVoT92HId0g==", "dev": true }, + "microbundle": { + "version": "0.12.0-next.6", + "resolved": "https://registry.npmjs.org/microbundle/-/microbundle-0.12.0-next.6.tgz", + "integrity": "sha512-IoH9ooNAKAjg+kLXW9Q7/9viaccnRdRTq/IQoM9rRkfxJl+DoW8RPftxg2kEFKjbecQB0mGk/dvBQa208ayBJw==", + "dev": true, + "requires": { + "@babel/core": "^7.5.5", + "@babel/plugin-proposal-class-properties": "7.5.5", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@babel/plugin-transform-flow-strip-types": "^7.4.4", + "@babel/plugin-transform-react-jsx": "^7.3.0", + "@babel/preset-env": "^7.5.5", + "@babel/preset-flow": "^7.0.0", + "asyncro": "^3.0.0", + "autoprefixer": "^9.6.1", + "babel-plugin-macros": "^2.4.2", + "babel-plugin-transform-async-to-promises": "^0.8.14", + "babel-plugin-transform-replace-expressions": "^0.2.0", + "brotli-size": "^4.0.0", + "camelcase": "^5.3.1", + "cssnano": "^4.1.10", + "es6-promisify": "^6.0.1", + "filesize": "^4.1.2", + "gzip-size": "^5.1.1", + "kleur": "^3.0.3", + "lodash.merge": "^4.6.2", + "module-details-from-path": "^1.0.3", + "pretty-bytes": "^5.3.0", + "rollup": "^1.19.4", + "rollup-plugin-alias": "^2.0.0", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-bundle-size": "^1.0.1", + "rollup-plugin-commonjs": "^10.0.2", + "rollup-plugin-es3": "^1.1.0", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-postcss": "^2.0.3", + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-typescript2": "^0.23.0", + "sade": "^1.6.1", + "tiny-glob": "^0.2.6", + "tslib": "^1.10.0", + "typescript": "^3.5.3" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, "microcli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/microcli/-/microcli-1.3.3.tgz", @@ -2974,24 +6739,24 @@ } }, "mime": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz", - "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", "dev": true }, "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==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", "dev": true }, "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==", + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", "dev": true, "requires": { - "mime-db": "~1.33.0" + "mime-db": "1.42.0" } }, "minimatch": { @@ -3004,15 +6769,14 @@ } }, "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -3034,6 +6798,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, "requires": { "minimist": "0.0.8" }, @@ -3041,10 +6806,17 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true } } }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=", + "dev": true + }, "morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -3058,6 +6830,12 @@ "on-headers": "~1.0.1" } }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3071,9 +6849,9 @@ "dev": true }, "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "dev": true, "optional": true }, @@ -3096,6 +6874,12 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -3103,17 +6887,59 @@ "dev": true }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, - "node-fetch": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz", - "integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==", + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", + "dev": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "node-releases": { + "version": "1.1.41", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.41.tgz", + "integrity": "sha512-+IctMa7wIs8Cfsa8iYzeaLTFwv5Y4r5jZud+4AnfymzeEXKBCavFX0KBgzVaPVqf0ywa6PrO8/b+bPqdwjGBSg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -3124,13 +6950,13 @@ } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } @@ -3144,18 +6970,165 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, "null-check": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", "dev": true }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3199,6 +7172,18 @@ } } }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -3208,6 +7193,18 @@ "isobject": "^3.0.0" } }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", @@ -3220,6 +7217,16 @@ "isobject": "^3.0.0" } }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -3229,6 +7236,18 @@ "isobject": "^3.0.1" } }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "omelette": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/omelette/-/omelette-0.4.5.tgz", @@ -3276,28 +7295,28 @@ "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=", + "dev": true + } } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } + "word-wrap": "~1.2.3" } }, "os-tmpdir": { @@ -3306,22 +7325,76 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "parse": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-2.4.0.tgz", - "integrity": "sha512-H6PDiML1CFnswInFL9dAszPaHkXH9eEzH1sGKUs/sf0R9h8eZQjYO8QoO/xObZJe3gHJMHx4StGE/zs4G3NLDw==", + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, "requires": { - "@babel/runtime": "7.4.3", - "uuid": "3.3.2", - "ws": "6.2.1", + "p-reduce": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-queue": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", + "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==", + "dev": true + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-2.9.1.tgz", + "integrity": "sha512-vLuJEtkZQGQZk0sZqETWmEx6IOhQYowyV4+bd0z4Fn3/FalC+RgisGOJIRdlKfjCKcinDBMtEttcMB5pg4v44A==", + "requires": { + "@babel/runtime": "7.7.2", + "@babel/runtime-corejs3": "7.7.2", + "uuid": "3.3.3", + "ws": "7.2.0", "xmlhttprequest": "1.8.0" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } } }, "parse-json": { @@ -3333,6 +7406,12 @@ "error-ex": "^1.2.0" } }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -3393,6 +7472,18 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "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==", + "dev": true + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -3413,27 +7504,35 @@ "through": "~2.3" } }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, "phaser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.17.0.tgz", - "integrity": "sha512-lnpOqchC4eHkt7zDljPjGUfGi2agDhrdXPR/DdXTHSkMWywvhF6/lFIkWlyfZm6S4kmmQp4sovUYn6nrOcNKPw==", + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.20.1.tgz", + "integrity": "sha512-P4x7zfMHdLKu8aPaADg+zTPNvFRYNHqmNqWZEUGw4WnbXZ/ACK+VR8k/11OzYxckEKBnWsqASDMKG9rzEC1rqg==", "requires": { - "dts-dom": "^3.2.0", "eventemitter3": "^3.1.0", "exports-loader": "^0.7.0", "imports-loader": "^0.8.0", - "jsdoc": "^3.6.1", - "path": "^0.12.7", - "typescript": "^3.4.5" - }, - "dependencies": { - "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==" - } + "path": "^0.12.7" } }, + "picomatch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", + "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==", + "dev": true + }, + "pidtree": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", + "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -3455,39 +7554,907 @@ "pinkie": "^2.0.0" } }, - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", "dev": true, "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "node-modules-regexp": "^1.0.0" } }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + } + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.23.tgz", + "integrity": "sha512-hOlMf3ouRIFXD+j2VJecwssTwbvsPGJVMzupptg+85WA+i7MwyrydmQAgY3R+m0Bc0exunhbJmijy8u8+vufuQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-1.4.1.tgz", + "integrity": "sha512-btTrbK+Xc3NBuYF8TPBjCMRSp5h6NoQ1iVZ6WiDQENIze6KIYCSf0+UFQuV3yJ7gRHA+4AAtF8i2jRvUpbBMMg==", + "dev": true, + "requires": { + "css-modules-loader-core": "^1.1.0", + "generic-names": "^1.0.3", + "lodash.camelcase": "^4.3.0", + "postcss": "^7.0.1", + "string-hash": "^1.1.1" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "pretty-bytes": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", + "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==", + "dev": true + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, "process-pool": { @@ -3500,6 +8467,45 @@ "lodash": "^3.3.1", "rewire": "^2.1.3", "source-map": "^0.4.0" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha1-LMfr6Vn8OmYZwEq029yeRS2GS70=", + "dev": true + }, + "prompts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.0.tgz", + "integrity": "sha512-NfbbPPg/74fT7wk2XYQ7hAIp9zJyZp5Fu19iRbORqqy1BhtrkZ0fPafBU+7bmn8ie69DpT0R6QpJIN2oisYjJg==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.3" } }, "proxy-middleware": { @@ -3514,16 +8520,50 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, "raw-body": { @@ -3538,6 +8578,12 @@ "unpipe": "1.0.0" } }, + "react-is": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -3572,14 +8618,6 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - } } }, "readdirp": { @@ -3593,6 +8631,15 @@ "readable-stream": "^2.0.2" } }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -3612,6 +8659,35 @@ "strip-indent": "^1.0.1" } }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -3622,23 +8698,39 @@ "safe-regex": "^1.1.0" } }, - "remap-istanbul": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.13.0.tgz", - "integrity": "sha512-rS5ZpVAx3fGtKZkiBe1esXg5mKYbgW9iz8kkADFt3p6lo3NsBBUX1q6SwdhwUtYCGnr7nK6gRlbYK3i8R0jbRA==", + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", "dev": true, "requires": { - "istanbul": "0.4.5", - "minimatch": "^3.0.4", - "plugin-error": "^1.0.1", - "source-map": "0.6.1", - "through2": "3.0.0" + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } } @@ -3670,31 +8762,124 @@ "is-finite": "^1.0.0" } }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, - "requizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.2.tgz", - "integrity": "sha512-oJ6y7JcUJkblRGhMByGNcszeLgU0qDxNKFCiUZR1XyzHyVsev+Mxb1tyygxLd1ORsKee1SA5BInFdUwY64GE/A==", - "requires": { - "lodash": "^4.17.11" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - } - } + "reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=", + "dev": true }, "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz", + "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "dev": true }, "resolve-url": { @@ -3716,20 +8901,312 @@ "dev": true }, "rfdc": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz", - "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { "glob": "^7.1.3" } }, + "rollup": { + "version": "1.27.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.27.4.tgz", + "integrity": "sha512-UaGNOIax/Ixfd92CAAanUilx2RSkkwEfC1lCTw1eL5Re6NURWgX66ARZt5+3px4kYnpSwzyOot4r18c2b+QgJQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/node": "*", + "acorn": "^7.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true + } + } + }, + "rollup-plugin-alias": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-alias/-/rollup-plugin-alias-2.2.0.tgz", + "integrity": "sha512-9ZK410qeFed4gGrHoojBpxLsHF74vPgsheGg9JRW5RbALAxqdvJbd357mSqWBqUrBfRVnZnNUXTZdYLxxQEA5A==", + "dev": true, + "requires": { + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "rollup-plugin-babel": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.3.3.tgz", + "integrity": "sha512-tKzWOCmIJD/6aKNz0H1GMM+lW1q9KyFubbWzGiOG540zxPPifnEAHTZwjo0g991Y+DyOZcLqBgqOdqazYE5fkw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-bundle-size": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-bundle-size/-/rollup-plugin-bundle-size-1.0.3.tgz", + "integrity": "sha512-aWj0Pvzq90fqbI5vN1IvUrlf4utOqy+AERYxwWjegH1G8PzheMnrRIgQ5tkwKVtQMDP0bHZEACW/zLDF+XgfXQ==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "maxmin": "^2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-es3": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-es3/-/rollup-plugin-es3-1.1.0.tgz", + "integrity": "sha512-jTMqQgMZ/tkjRW4scf4ln5c0OiTSi+Lx/IEyFd41ldgGoLvvg9AQxmVOl93+KaoyB7XRYToYjiHDvO40NPF/fA==", + "dev": true, + "requires": { + "magic-string": "^0.22.4" + }, + "dependencies": { + "magic-string": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "dev": true, + "requires": { + "vlq": "^0.2.2" + } + } + } + }, + "rollup-plugin-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz", + "integrity": "sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow==", + "dev": true, + "requires": { + "rollup-pluginutils": "^2.5.0" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-postcss": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-2.0.3.tgz", + "integrity": "sha512-d12oKl6za/GGXmlytzVPzzTdPCKgti/Kq2kNhtfm5vv9hkNbyrTvizMBm6zZ5rRWX/sIWl3znjIJ8xy6Hofoeg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "concat-with-sourcemaps": "^1.0.5", + "cssnano": "^4.1.8", + "import-cwd": "^2.1.0", + "p-queue": "^2.4.2", + "pify": "^3.0.0", + "postcss": "^7.0.14", + "postcss-load-config": "^2.0.0", + "postcss-modules": "^1.4.1", + "promise.series": "^0.2.0", + "reserved-words": "^0.1.2", + "resolve": "^1.5.0", + "rollup-pluginutils": "^2.0.1", + "style-inject": "^0.3.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "rollup-plugin-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.1.2.tgz", + "integrity": "sha512-sWKBCOS+vUkRtHtEiJPAf+WnBqk/C402fBD9AVHxSIXMqjsY7MnYWKYEUqGixtr0c8+1DjzUEPlNgOYQPVrS1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "jest-worker": "^24.6.0", + "rollup-pluginutils": "^2.8.1", + "serialize-javascript": "^1.7.0", + "terser": "^4.1.0" + } + }, + "rollup-plugin-typescript2": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.23.0.tgz", + "integrity": "sha512-LocTdy/rtp7UVoQcxqO3nIDjuI6AhfmiO/iNTx0k3uGRGPFQzlAyw5hEFNMpAT2tlpoGqawRnOT9OCePuwfZ5w==", + "dev": true, + "requires": { + "fs-extra": "8.1.0", + "resolve": "1.11.1", + "rollup-pluginutils": "2.8.1", + "tslib": "1.10.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "rollup-pluginutils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz", + "integrity": "sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + } + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, "runjs": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/runjs/-/runjs-4.4.2.tgz", @@ -3742,6 +9219,15 @@ "omelette": "0.4.5" } }, + "sade": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.6.1.tgz", + "integrity": "sha512-USHm9quYNmJwFwhOnEuJohdnBhUOKV1mhL0koHSJMLJaesRX0nuDuzbWmtUBbUmXkwTalLtUBzDlEnU940BiQA==", + "dev": true, + "requires": { + "mri": "^1.1.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3763,16 +9249,39 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "send": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.0.tgz", - "integrity": "sha512-NYR0jCuwnBaGA2X5bO3+QDZmmJ+PUCvFCRTED5nx9l/BK3Pr8mD8Ryvk9bw08JJUdXxt2u+tVIGoqJPrHWGqSA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "dev": true, "requires": { "debug": "2.6.9", @@ -3786,7 +9295,7 @@ "mime": "1.6.0", "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", + "range-parser": "~1.2.1", "statuses": "~1.5.0" }, "dependencies": { @@ -3804,6 +9313,12 @@ } } }, + "serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -3839,10 +9354,16 @@ } } }, - "set-value": { + "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -3868,6 +9389,27 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "dev": true }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "shelljs": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", @@ -3879,12 +9421,47 @@ "rechoir": "^0.6.2" } }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "sisteransi": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.4.tgz", + "integrity": "sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -4103,13 +9680,9 @@ } }, "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", + "integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=" }, "source-map-resolve": { "version": "0.5.2", @@ -4125,9 +9698,9 @@ } }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -4148,10 +9721,16 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "sourcemap-codec": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", + "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", + "dev": true + }, "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -4159,9 +9738,9 @@ } }, "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", "dev": true }, "spdx-expression-parse": { @@ -4175,9 +9754,9 @@ } }, "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, "split": { @@ -4201,7 +9780,37 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true }, "static-extend": { "version": "0.1.2", @@ -4230,6 +9839,12 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, "stream-combiner": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", @@ -4246,25 +9861,25 @@ "dev": true }, "streamroller": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.4.tgz", - "integrity": "sha512-Wc2Gm5ygjSX8ZpW9J7Y9FwiSzTlKSvcl0FTTMd3rn7RoxDXpBW+xD9TY5sWL2n0UR61COB0LG1BQvN6nTUQbLQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", "dev": true, "requires": { - "async": "^2.6.1", + "async": "^2.6.2", "date-format": "^2.0.0", - "debug": "^3.1.0", - "fs-extra": "^7.0.0", - "lodash": "^4.17.10" + "debug": "^3.2.6", + "fs-extra": "^7.0.1", + "lodash": "^4.17.14" }, "dependencies": { "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "dev": true, "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.14" } }, "debug": { @@ -4276,40 +9891,89 @@ "ms": "^2.1.1" } }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, + "string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "string.prototype.padend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", + "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.4.3", + "function-bind": "^1.0.2" + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4319,6 +9983,15 @@ "safe-buffer": "~5.1.0" } }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", @@ -4328,6 +10001,12 @@ "is-utf8": "^0.2.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", @@ -4337,61 +10016,109 @@ "get-stdin": "^4.0.1" } }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==" + "style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } }, "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "dev": true, "requires": { - "has-flag": "^2.0.0" + "has-flag": "^1.0.0" + } + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" }, "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=" - }, - "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", - "dev": true, - "requires": { - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - } - } + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true }, "terser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", - "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.0.tgz", + "integrity": "sha512-oDG16n2WKm27JO8h4y/w3iqBGAOSCtq7k8dRmrn4Wf9NouL0b2WpMHGChFGZq4nFAQy1FsNJrVQHfurXOSTmOA==", "dev": true, "requires": { - "commander": "^2.19.0", + "commander": "^2.20.0", "source-map": "~0.6.1", - "source-map-support": "~0.5.10" + "source-map-support": "~0.5.12" }, "dependencies": { "source-map": { @@ -4402,20 +10129,136 @@ } } }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "through2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", - "integrity": "sha512-8B+sevlqP4OiCjonI1Zw03Sf8PuV1eRsYQgLad5eonILOdyeRsY27A/2Ze8IlvlMvq31OH+3fz/styI7Ya62yQ==", + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tiny-glob": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.6.tgz", + "integrity": "sha512-A7ewMqPu1B5PWwC3m7KVgAu96Ch5LA0w4SnEN/LbDREj/gAD0nPWboRbn8YoP9ISZXqeNAlMvKSKoEuhcfK3Pw==", "dev": true, "requires": { - "readable-stream": "2 || 3", - "xtend": "~4.0.1" + "globalyzer": "^0.1.0", + "globrex": "^0.1.1" + } + }, + "tk-base": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tk-base/-/tk-base-0.2.5.tgz", + "integrity": "sha512-WbzDdcGW1Vh8mZKOPgymaQ0ZluHqtRBxkU/s/Sn/b3Xm8XF07w3KkZpwumQqa++4Zdmh5HV/nDP2W/4IWuL+NA==", + "dev": true, + "requires": { + "@types/jest": "^24.0.22", + "@types/node": "^12.12.6", + "jest": "^24.9.0", + "live-server": "^1.2.1", + "microbundle": "^0.12.0-next.6", + "npm-run-all": "^4.1.5", + "ts-jest": "^24.1.0", + "ts-node": "^8.4.1", + "typescript": "^3.7.2" } }, "tmp": { @@ -4427,12 +10270,24 @@ "os-tmpdir": "~1.0.2" } }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", "dev": true }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -4481,12 +10336,109 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, + "ts-jest": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.1.0.tgz", + "integrity": "sha512-HEGfrIEAZKfu1pkaxB9au17b1d9b56YZSqz5eCVE8mX68+5reOvlM93xGOzzCREIov9mdH7JBG+s0UyNAqr0tQ==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "ts-node": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.2.tgz", + "integrity": "sha512-W1DK/a6BGoV/D4x/SXXm6TSQx6q3blECUzd5TN+j56YEMX3yPVMpHsICLedUw3DvGF3aTQ8hfdR9AKMaHjIi+A==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -4504,44 +10456,22 @@ "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.24" - }, - "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - } } }, "typescript": { - "version": "3.5.0-rc", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.0-rc.tgz", - "integrity": "sha512-8Os3bqTeHc6bf+bkPFL3O/pb09j8SbDa2LUBxTXWpZlcHUW9ziGuiEFiqMcArkbAjGLqEzshkl4zvxhb0gVPuQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, "uglify-js": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.12.tgz", - "integrity": "sha512-KeQesOpPiZNgVwJj8Ge3P4JYbQHUdZzpx6Fahy6eKAYRSV4zhVmLXoC+JtOeYxcHCHTve8RG1ZGdTvpeOUM26Q==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { @@ -4560,46 +10490,58 @@ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -4607,9 +10549,9 @@ "dev": true }, "unix-crypt-td-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.0.0.tgz", - "integrity": "sha1-HAgkFQSBvHoB1J6Y8exmjYJBLzs=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", "dev": true }, "unpipe": { @@ -4618,6 +10560,12 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4659,23 +10607,26 @@ } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -4706,6 +10657,16 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4713,15 +10674,14 @@ "dev": true }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { "spdx-correct": "^3.0.0", @@ -4734,19 +10694,67 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, + "vendors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", + "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", + "dev": true + }, "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", "dev": true }, - "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", "dev": true, "requires": { - "http-parser-js": ">=0.4.0", + "browser-process-hrtime": "^0.1.2" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, @@ -4756,6 +10764,32 @@ "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", "dev": true }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4765,30 +10799,65 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", "dev": true }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, "requires": { - "async-limiter": "~1.0.0" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" } }, - "xmlcreate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.1.tgz", - "integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==" + "ws": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", + "requires": { + "async-limiter": "^1.0.0" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true }, "xmlhttprequest": { "version": "1.8.0", @@ -4801,10 +10870,10 @@ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", "dev": true }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, "yallist": { @@ -4813,11 +10882,73 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yaml": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz", + "integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.6.3" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index 8dfbc8b..e6b5c8d 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,31 @@ "name": "spacetac", "version": "0.1.0", "description": "A tactical RPG set in space", - "main": "out/build.js", + "main": "dist/spacetac.umd.js", "scripts": { - "build": "run build", - "test": "run ci", - "start": "run continuous" + "build": "microbundle build -f modern,umd", + "test": "jest", + "start": "run continuous", + "normalize": "tk-base", + "dev": "run-p dev:*", + "dev:test": "jest --watchAll", + "dev:build": "microbundle watch -f modern,umd", + "dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist", + "prepare": "npm run build", + "prepublishOnly": "npm test" }, "repository": { "type": "git", - "url": "https://code.thunderk.net/michael/spacetac.git" + "url": "https://code.thunderk.net/games/spacetac.git" }, - "author": "Michael Lemaire", - "license": "MIT", + "author": { + "name": "Michaël Lemaire", + "url": "https://thunderk.net" + }, + "license": "ISC", "devDependencies": { "@types/jasmine": "^3.3.12", - "codecov": "^3.4.0", + "@types/parse": "^2.9.0", "gamefroot-texture-packer": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff", "glob": "^7.1.4", "glob-watcher": "^5.0.3", @@ -27,20 +37,31 @@ "karma-coverage": "^1.1.2", "karma-jasmine": "^2.0.1", "karma-spec-reporter": "^0.0.32", - "live-server": "1.2.1", "process-pool": "^0.3.5", - "remap-istanbul": "^0.13.0", "runjs": "^4.4.2", "shelljs": "^0.8.3", - "terser": "^3.17.0", - "typescript": "^3.5.0-rc" + "tk-base": "^0.2.5" }, "dependencies": { "parse": "^2.4.0", - "phaser": "^3.17.0" + "phaser": "^3.20.1" }, "dependenciesMap": { "parse": "dist/parse.min.js", "phaser": "dist/phaser.min.js" - } + }, + "source": "src/index.ts", + "module": "dist/spacetac.modern.js", + "types": "dist/index.d.ts", + "files": [ + "/src", + "/dist" + ], + "bugs": { + "url": "https://gitlab.com/thunderk/spacetac/issues" + }, + "homepage": "https://code.thunderk.net/tslib/spacetac", + "keywords": [ + "typescript" + ] } diff --git a/src/MainUI.spec.ts b/src/MainUI.spec.ts index f566acf..ff1d186 100644 --- a/src/MainUI.spec.ts +++ b/src/MainUI.spec.ts @@ -1,41 +1,42 @@ -/// +import { testing } from "./common/Testing"; +import { bool } from "./common/Tools"; +import { GameSession } from "./core/GameSession"; +import { setupEmptyView } from "./ui/TestGame"; -module TK.SpaceTac.UI.Specs { - class FakeStorage { - data: any = {} - getItem(name: string) { - return this.data[name]; - } - setItem(name: string, value: string) { - this.data[name] = value; - } - } - - testing("MainUI", test => { - let testgame = setupEmptyView(test); - - test.case("saves games in local browser storage", check => { - let ui = testgame.ui; - ui.storage = new FakeStorage(); - - let result = ui.loadGame("spacetac-test-save"); - check.equals(result, false); - - ui.session.startNewGame(); - let systems = ui.session.universe.stars.length; - let links = ui.session.universe.starlinks.length; - - result = ui.saveGame("spacetac-test-save"); - check.equals(result, true); - check.equals(bool(ui.storage.getItem("spacetac-test-save")), true); - - ui.session = new GameSession(); - check.notsame(ui.session.universe.stars.length, systems); - - result = ui.loadGame("spacetac-test-save"); - check.equals(result, true); - check.same(ui.session.universe.stars.length, systems); - check.same(ui.session.universe.starlinks.length, links); - }); - }); +class FakeStorage { + data: any = {} + getItem(name: string) { + return this.data[name]; + } + setItem(name: string, value: string) { + this.data[name] = value; + } } + +testing("MainUI", test => { + let testgame = setupEmptyView(test); + + test.case("saves games in local browser storage", check => { + let ui = testgame.ui; + ui.storage = new FakeStorage(); + + let result = ui.loadGame("spacetac-test-save"); + check.equals(result, false); + + ui.session.startNewGame(); + let systems = ui.session.universe.stars.length; + let links = ui.session.universe.starlinks.length; + + result = ui.saveGame("spacetac-test-save"); + check.equals(result, true); + check.equals(bool(ui.storage.getItem("spacetac-test-save")), true); + + ui.session = new GameSession(); + check.notsame(ui.session.universe.stars.length, systems); + + result = ui.loadGame("spacetac-test-save"); + check.equals(result, true); + check.same(ui.session.universe.stars.length, systems); + check.same(ui.session.universe.starlinks.length, links); + }); +}); diff --git a/src/MainUI.ts b/src/MainUI.ts index 3d0cbbf..53d0cd0 100644 --- a/src/MainUI.ts +++ b/src/MainUI.ts @@ -1,217 +1,208 @@ /// -declare var global: any; -declare var module: any; +import { RandomGenerator } from "./common/RandomGenerator" +import { iteritems, keys } from "./common/Tools" +import { GameSession } from "./core/GameSession" +import { AssetLoading } from "./ui/AssetLoading" +import { BaseView } from "./ui/BaseView" +import { BattleView } from "./ui/battle/BattleView" +import { Boot } from "./ui/Boot" +import { FleetCreationView } from "./ui/character/FleetCreationView" +import { AudioManager } from "./ui/common/AudioManager" +import { IntroView } from "./ui/intro/IntroView" +import { UniverseMapView } from "./ui/map/UniverseMapView" +import { MainMenu } from "./ui/menu/MainMenu" +import { GameOptions } from "./ui/options/GameOptions" +import { Router } from "./ui/Router" -if (typeof window != "undefined") { - // If jasmine is not present, ignore describe - (window).describe = (window).describe || function () { }; -} else { - if (typeof global != "undefined") { - // In node, does not extend Phaser classes - var handler = { - get(target: any, name: any): any { - return new Proxy(function () { }, handler); - } - } - global.Phaser = new Proxy(function () { }, handler); - } +/** + * Main class to bootstrap the whole game + */ +export class MainUI extends Phaser.Game { + // Current game session + session: GameSession + session_token: string | null - if (typeof module != "undefined") { - module.exports = { TK }; - } -} - -module TK.SpaceTac { - /** - * Main class to bootstrap the whole game - */ - export class MainUI extends Phaser.Game { - // Current game session - session: GameSession - session_token: string | null - - // Audio manager - audio = new UI.Audio(this) - - // Game options - options = new UI.GameOptions(this) - - // Storage used - storage: Storage - - // Debug mode - debug = false - - // Current scaling - scaling = 1 - - constructor(private testmode = false) { - super({ - width: 1920, - height: 1080, - type: testmode ? Phaser.CANVAS : Phaser.WEBGL, // cannot really use HEADLESS because of bugs - backgroundColor: '#000000', - parent: '-space-tac', - disableContextMenu: true, - scale: { - mode: Phaser.Scale.FIT, - autoCenter: Phaser.Scale.CENTER_BOTH - }, - }); - - this.storage = localStorage; - - this.session = new GameSession(); - this.session_token = null; - - if (!testmode) { - this.events.on("blur", () => { - this.scene.scenes.forEach(scene => this.scene.pause(scene)); - }); - this.events.on("focus", () => { - this.scene.scenes.forEach(scene => this.scene.resume(scene)); - }); - - this.scene.add('boot', UI.Boot); - this.scene.add('loading', UI.AssetLoading); - this.scene.add('mainmenu', UI.MainMenu); - this.scene.add('router', UI.Router); - this.scene.add('battle', UI.BattleView); - this.scene.add('intro', UI.IntroView); - this.scene.add('creation', UI.FleetCreationView); - this.scene.add('universe', UI.UniverseMapView); - - this.goToScene('boot'); - } - } - - get isTesting(): boolean { - return this.testmode; - } - - /** - * Reset the game session - */ - resetSession(): void { - this.session = new GameSession(); - this.session_token = null; - } - - /** - * Display a popup message in current view - */ - displayMessage(message: string) { - iteritems(this.scene.keys, (key: string, scene: UI.BaseView) => { - if (scene.messages && this.scene.isVisible(key)) { - scene.messages.addMessage(message); - } - }); - } - - /** - * Change the active scene - */ - goToScene(name: string): void { - keys(this.scene.keys).forEach(key => { - if (this.scene.isActive(key) || this.scene.isVisible(key)) { - this.scene.stop(key); - } - }); - this.scene.start(name); - } - - /** - * Quit the current session, and go back to mainmenu - */ - quitGame() { - this.resetSession(); - this.goToScene('router'); - } - - /** - * Save current game in local browser storage - */ - saveGame(name = "spacetac-savegame"): boolean { - if (typeof this.storage != "undefined") { - this.storage.setItem(name, this.session.saveToString()); - this.displayMessage("Game saved"); - return true; - } else { - this.displayMessage("Your browser does not support saving"); - return false; - } - } - - /** - * Set the current game session, and redirect to view router - */ - setSession(session: GameSession, token?: string): void { - this.session = session; - this.session_token = token || null; - this.goToScene("router"); - } - - /** - * Load current game from local browser storage - */ - loadGame(name = "spacetac-savegame"): boolean { - if (typeof this.storage != "undefined") { - var loaded = this.storage.getItem(name); - if (loaded) { - this.session = GameSession.loadFromString(loaded); - this.session_token = null; - console.log("Game loaded"); - return true; - } else { - console.warn("No saved game found"); - return false; - } - } else { - console.error("localStorage not available"); - return false; - } - } - - /** - * Get an hopefully unique device identifier - */ - getDeviceId(): string | null { - if (this.storage) { - const key = "spacetac-device-id"; - let stored = this.storage.getItem(key); - if (stored) { - return stored; - } else { - let generated = RandomGenerator.global.id(20); - this.storage.setItem(key, generated); - return generated; - } - } else { - return null; - } - } - - /** - * Check if the game is currently fullscreen - */ - isFullscreen(): boolean { - return this.scale.isFullscreen; - } - - /** - * Toggle fullscreen mode. - * - * Returns true if the result is fullscreen - */ - toggleFullscreen(active: boolean | null = null): boolean { - if (active === false || (active !== true && this.isFullscreen())) { - this.scale.stopFullscreen(); - return false; - } else { - this.scale.startFullscreen(); - return true; - } - } + // Audio manager + audio = new AudioManager(this) + + // Game options + options = new GameOptions(this) + + // Storage used + storage: Storage + + // Debug mode + debug = false + + // Current scaling + scaling = 1 + + constructor(private testmode = false) { + super({ + width: 1920, + height: 1080, + type: testmode ? Phaser.CANVAS : Phaser.WEBGL, // cannot really use HEADLESS because of bugs + backgroundColor: '#000000', + parent: '-space-tac', + disableContextMenu: true, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH + }, + }); + + this.storage = localStorage; + + this.session = new GameSession(); + this.session_token = null; + + if (!testmode) { + this.events.on("blur", () => { + this.scene.scenes.forEach(scene => this.scene.pause(scene)); + }); + this.events.on("focus", () => { + this.scene.scenes.forEach(scene => this.scene.resume(scene)); + }); + + this.scene.add('boot', Boot); + this.scene.add('loading', AssetLoading); + this.scene.add('mainmenu', MainMenu); + this.scene.add('router', Router); + this.scene.add('battle', BattleView); + this.scene.add('intro', IntroView); + this.scene.add('creation', FleetCreationView); + this.scene.add('universe', UniverseMapView); + + this.goToScene('boot'); } + } + + get isTesting(): boolean { + return this.testmode; + } + + /** + * Reset the game session + */ + resetSession(): void { + this.session = new GameSession(); + this.session_token = null; + } + + /** + * Display a popup message in current view + */ + displayMessage(message: string) { + iteritems(this.scene.keys, (key: string, scene: BaseView) => { + if (scene.messages && this.scene.isVisible(key)) { + scene.messages.addMessage(message); + } + }); + } + + /** + * Change the active scene + */ + goToScene(name: string): void { + keys(this.scene.keys).forEach(key => { + if (this.scene.isActive(key) || this.scene.isVisible(key)) { + this.scene.stop(key); + } + }); + this.scene.start(name); + } + + /** + * Quit the current session, and go back to mainmenu + */ + quitGame() { + this.resetSession(); + this.goToScene('router'); + } + + /** + * Save current game in local browser storage + */ + saveGame(name = "spacetac-savegame"): boolean { + if (typeof this.storage != "undefined") { + this.storage.setItem(name, this.session.saveToString()); + this.displayMessage("Game saved"); + return true; + } else { + this.displayMessage("Your browser does not support saving"); + return false; + } + } + + /** + * Set the current game session, and redirect to view router + */ + setSession(session: GameSession, token?: string): void { + this.session = session; + this.session_token = token || null; + this.goToScene("router"); + } + + /** + * Load current game from local browser storage + */ + loadGame(name = "spacetac-savegame"): boolean { + if (typeof this.storage != "undefined") { + var loaded = this.storage.getItem(name); + if (loaded) { + this.session = GameSession.loadFromString(loaded); + this.session_token = null; + console.log("Game loaded"); + return true; + } else { + console.warn("No saved game found"); + return false; + } + } else { + console.error("localStorage not available"); + return false; + } + } + + /** + * Get an hopefully unique device identifier + */ + getDeviceId(): string | null { + if (this.storage) { + const key = "spacetac-device-id"; + let stored = this.storage.getItem(key); + if (stored) { + return stored; + } else { + let generated = RandomGenerator.global.id(20); + this.storage.setItem(key, generated); + return generated; + } + } else { + return null; + } + } + + /** + * Check if the game is currently fullscreen + */ + isFullscreen(): boolean { + return this.scale.isFullscreen; + } + + /** + * Toggle fullscreen mode. + * + * Returns true if the result is fullscreen + */ + toggleFullscreen(active: boolean | null = null): boolean { + if (active === false || (active !== true && this.isFullscreen())) { + this.scale.stopFullscreen(); + return false; + } else { + this.scale.startFullscreen(); + return true; + } + } } diff --git a/src/common/DiffLog.spec.ts b/src/common/DiffLog.spec.ts index 630d106..6907d78 100644 --- a/src/common/DiffLog.spec.ts +++ b/src/common/DiffLog.spec.ts @@ -1,230 +1,228 @@ -module TK.Specs { - class TestState { - counter = 0 - } +class TestState { + counter = 0 +} - class TestDiff extends Diff { - private value: number - constructor(value = 1) { - super(); - this.value = value; - } - apply(state: TestState) { - state.counter += this.value; - } - getReverse() { - return new TestDiff(-this.value); - } - } +class TestDiff extends Diff { + private value: number + constructor(value = 1) { + super(); + this.value = value; + } + apply(state: TestState) { + state.counter += this.value; + } + getReverse() { + return new TestDiff(-this.value); + } +} - testing("DiffLog", test => { - test.case("stores sequential events", check => { - let log = new DiffLog(); - check.equals(log.count(), 0); - check.equals(log.get(0), null); - check.equals(log.get(1), null); - check.equals(log.get(2), null); +testing("DiffLog", test => { + test.case("stores sequential events", check => { + let log = new DiffLog(); + check.equals(log.count(), 0); + check.equals(log.get(0), null); + check.equals(log.get(1), null); + check.equals(log.get(2), null); - log.add(new TestDiff(2)); - check.equals(log.count(), 1); - check.equals(log.get(0), new TestDiff(2)); - check.equals(log.get(1), null); - check.equals(log.get(2), null); + log.add(new TestDiff(2)); + check.equals(log.count(), 1); + check.equals(log.get(0), new TestDiff(2)); + check.equals(log.get(1), null); + check.equals(log.get(2), null); - log.add(new TestDiff(-4)); - check.equals(log.count(), 2); - check.equals(log.get(0), new TestDiff(2)); - check.equals(log.get(1), new TestDiff(-4)); - check.equals(log.get(2), null); + log.add(new TestDiff(-4)); + check.equals(log.count(), 2); + check.equals(log.get(0), new TestDiff(2)); + check.equals(log.get(1), new TestDiff(-4)); + check.equals(log.get(2), null); - log.clear(1); - check.equals(log.count(), 1); - check.equals(log.get(0), new TestDiff(2)); + log.clear(1); + check.equals(log.count(), 1); + check.equals(log.get(0), new TestDiff(2)); - log.clear(); - check.equals(log.count(), 0); - }) - }) + log.clear(); + check.equals(log.count(), 0); + }) +}) - testing("DiffLogClient", test => { - test.case("adds diffs to the log", check => { - let log = new DiffLog(); - let state = new TestState(); - let client = new DiffLogClient(state, log); +testing("DiffLogClient", test => { + test.case("adds diffs to the log", check => { + let log = new DiffLog(); + let state = new TestState(); + let client = new DiffLogClient(state, log); - check.equals(client.atEnd(), true, "client is empty, should be at end"); - check.equals(log.count(), 0, "log is empty initially"); - check.equals(state.counter, 0, "initial state is 0"); + check.equals(client.atEnd(), true, "client is empty, should be at end"); + check.equals(log.count(), 0, "log is empty initially"); + check.equals(state.counter, 0, "initial state is 0"); - client.add(new TestDiff(3)); - check.equals(client.atEnd(), true, "client still at end"); - check.equals(log.count(), 1, "diff added to log"); - check.equals(state.counter, 3, "diff applied to state"); + client.add(new TestDiff(3)); + check.equals(client.atEnd(), true, "client still at end"); + check.equals(log.count(), 1, "diff added to log"); + check.equals(state.counter, 3, "diff applied to state"); - client.add(new TestDiff(2), false); - check.equals(client.atEnd(), false, "client lapsing behind"); - check.equals(log.count(), 2, "diff added to log"); - check.equals(state.counter, 3, "diff not applied to state"); - }) + client.add(new TestDiff(2), false); + check.equals(client.atEnd(), false, "client lapsing behind"); + check.equals(log.count(), 2, "diff added to log"); + check.equals(state.counter, 3, "diff not applied to state"); + }) - test.case("initializes at current state (end of log)", check => { - let state = new TestState(); - let log = new DiffLog(); - log.add(new TestDiff(7)); - let client = new DiffLogClient(state, log); - check.equals(client.atStart(), false); - check.equals(client.atEnd(), true); - check.equals(state.counter, 0); - client.forward(); - check.equals(state.counter, 0); - client.backward(); - check.equals(state.counter, -7); - }) + test.case("initializes at current state (end of log)", check => { + let state = new TestState(); + let log = new DiffLog(); + log.add(new TestDiff(7)); + let client = new DiffLogClient(state, log); + check.equals(client.atStart(), false); + check.equals(client.atEnd(), true); + check.equals(state.counter, 0); + client.forward(); + check.equals(state.counter, 0); + client.backward(); + check.equals(state.counter, -7); + }) - test.case("moves forward or backward in the log", check => { - let log = new DiffLog(); - let state = new TestState(); - let client = new DiffLogClient(state, log); + test.case("moves forward or backward in the log", check => { + let log = new DiffLog(); + let state = new TestState(); + let client = new DiffLogClient(state, log); - log.add(new TestDiff(7)); - log.add(new TestDiff(-2)); - log.add(new TestDiff(4)); + log.add(new TestDiff(7)); + log.add(new TestDiff(-2)); + log.add(new TestDiff(4)); - check.equals(state.counter, 0, "initial state is 0"); - check.equals(client.atStart(), true, "client is at start"); - check.equals(client.atEnd(), false, "client is not at end"); + check.equals(state.counter, 0, "initial state is 0"); + check.equals(client.atStart(), true, "client is at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.forward(); - check.equals(state.counter, 7, "0+7 => 7"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), false, "client is not at end"); + client.forward(); + check.equals(state.counter, 7, "0+7 => 7"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.forward(); - check.equals(state.counter, 5, "7-2 => 5"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), false, "client is not at end"); + client.forward(); + check.equals(state.counter, 5, "7-2 => 5"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.forward(); - check.equals(state.counter, 9, "5+4 => 9"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), true, "client is at end"); + client.forward(); + check.equals(state.counter, 9, "5+4 => 9"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), true, "client is at end"); - client.forward(); - check.equals(state.counter, 9, "at end, still 9"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), true, "client is at end"); + client.forward(); + check.equals(state.counter, 9, "at end, still 9"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), true, "client is at end"); - client.backward(); - check.equals(state.counter, 5, "9-4=>5"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), false, "client is not at end"); + client.backward(); + check.equals(state.counter, 5, "9-4=>5"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.backward(); - check.equals(state.counter, 7, "5+2=>7"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), false, "client is not at end"); + client.backward(); + check.equals(state.counter, 7, "5+2=>7"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.backward(); - check.equals(state.counter, 0, "7-7=>0"); - check.equals(client.atStart(), true, "client is back at start"); - check.equals(client.atEnd(), false, "client is not at end"); + client.backward(); + check.equals(state.counter, 0, "7-7=>0"); + check.equals(client.atStart(), true, "client is back at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.backward(); - check.equals(state.counter, 0, "at start, still 0"); - check.equals(client.atStart(), true, "client is at start"); - check.equals(client.atEnd(), false, "client is not at end"); - }) + client.backward(); + check.equals(state.counter, 0, "at start, still 0"); + check.equals(client.atStart(), true, "client is at start"); + check.equals(client.atEnd(), false, "client is not at end"); + }) - test.case("jumps to start or end of the log", check => { - let log = new DiffLog(); - let state = new TestState(); - let client = new DiffLogClient(state, log); + test.case("jumps to start or end of the log", check => { + let log = new DiffLog(); + let state = new TestState(); + let client = new DiffLogClient(state, log); - client.add(new TestDiff(7)); - log.add(new TestDiff(-2)); - log.add(new TestDiff(4)); + client.add(new TestDiff(7)); + log.add(new TestDiff(-2)); + log.add(new TestDiff(4)); - check.equals(state.counter, 7, "initial state is 7"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), false, "client is not at end"); + check.equals(state.counter, 7, "initial state is 7"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), false, "client is not at end"); - client.jumpToEnd(); - check.equals(state.counter, 9, "7-2+4=>9"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), true, "client at end"); + client.jumpToEnd(); + check.equals(state.counter, 9, "7-2+4=>9"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), true, "client at end"); - client.jumpToEnd(); - check.equals(state.counter, 9, "still 9"); - check.equals(client.atStart(), false, "client is not at start"); - check.equals(client.atEnd(), true, "client at end"); + client.jumpToEnd(); + check.equals(state.counter, 9, "still 9"); + check.equals(client.atStart(), false, "client is not at start"); + check.equals(client.atEnd(), true, "client at end"); - client.jumpToStart(); - check.equals(state.counter, 0, "9-4+2-7=>0"); - check.equals(client.atStart(), true, "client is at start"); - check.equals(client.atEnd(), false, "client at not end"); + client.jumpToStart(); + check.equals(state.counter, 0, "9-4+2-7=>0"); + check.equals(client.atStart(), true, "client is at start"); + check.equals(client.atEnd(), false, "client at not end"); - client.jumpToStart(); - check.equals(state.counter, 0, "still 0"); - check.equals(client.atStart(), true, "client is at start"); - check.equals(client.atEnd(), false, "client at not end"); - }) + client.jumpToStart(); + check.equals(state.counter, 0, "still 0"); + check.equals(client.atStart(), true, "client is at start"); + check.equals(client.atEnd(), false, "client at not end"); + }) - test.case("truncate the log", check => { - let log = new DiffLog(); - let state = new TestState(); - let client = new DiffLogClient(state, log); + test.case("truncate the log", check => { + let log = new DiffLog(); + let state = new TestState(); + let client = new DiffLogClient(state, log); - client.add(new TestDiff(7)); - client.add(new TestDiff(3)); - client.add(new TestDiff(5)); + client.add(new TestDiff(7)); + client.add(new TestDiff(3)); + client.add(new TestDiff(5)); - check.in("initial state", check => { - check.equals(state.counter, 15, "state=15"); - check.equals(log.count(), 3, "count=3"); - }); + check.in("initial state", check => { + check.equals(state.counter, 15, "state=15"); + check.equals(log.count(), 3, "count=3"); + }); - client.backward(); + client.backward(); - check.in("after backward", check => { - check.equals(state.counter, 10, "state=10"); - check.equals(log.count(), 3, "count=3"); - }); + check.in("after backward", check => { + check.equals(state.counter, 10, "state=10"); + check.equals(log.count(), 3, "count=3"); + }); - client.truncate(); + client.truncate(); - check.in("after truncate", check => { - check.equals(state.counter, 10, "state=10"); - check.equals(log.count(), 2, "count=2"); - }); + check.in("after truncate", check => { + check.equals(state.counter, 10, "state=10"); + check.equals(log.count(), 2, "count=2"); + }); - client.truncate(); + client.truncate(); - check.in("after another truncate", check => { - check.equals(state.counter, 10, "state=10"); - check.equals(log.count(), 2, "count=2"); - }); - }) + check.in("after another truncate", check => { + check.equals(state.counter, 10, "state=10"); + check.equals(log.count(), 2, "count=2"); + }); + }) - test.acase("plays the log continuously", async check => { - let log = new DiffLog(); - let state = new TestState(); - let client = new DiffLogClient(state, log); + test.acase("plays the log continuously", async check => { + let log = new DiffLog(); + let state = new TestState(); + let client = new DiffLogClient(state, log); - let inter: number[] = []; - let promise = client.play(diff => { - inter.push((diff).value); - return Promise.resolve(); - }); + let inter: number[] = []; + let promise = client.play(diff => { + inter.push((diff).value); + return Promise.resolve(); + }); - log.add(new TestDiff(5)); - log.add(new TestDiff(-1)); - log.add(new TestDiff(2)); - client.stop(false); + log.add(new TestDiff(5)); + log.add(new TestDiff(-1)); + log.add(new TestDiff(2)); + client.stop(false); - await promise; + await promise; - check.equals(state.counter, 6); - check.equals(inter, [5, -1, 2]); - }) - }) -} \ No newline at end of file + check.equals(state.counter, 6); + check.equals(inter, [5, -1, 2]); + }) +}) diff --git a/src/common/DiffLog.ts b/src/common/DiffLog.ts index f5c06d3..f320583 100644 --- a/src/common/DiffLog.ts +++ b/src/common/DiffLog.ts @@ -1,249 +1,244 @@ +import { Timer } from "./Timer"; + /** - * Framework to maintain a state from a log of changes - * - * This allows for repeatable, serializable and revertable state modifications. + * Base class for a single diff. + * + * This represents an atomic change of the state, that can be applied, or reverted. */ -module TK { - /** - * Base class for a single diff. - * - * This represents an atomic change of the state, that can be applied, or reverted. - */ - export class Diff { - /** - * Apply the diff on a given state - * - * By default it does nothing - */ - apply(state: T): void { - } +export class Diff { + /** + * Apply the diff on a given state + * + * By default it does nothing + */ + apply(state: T): void { + } - /** - * Reverts the diff from a given state - * - * By default it applies the reverse event - */ - revert(state: T): void { - this.getReverse().apply(state); - } + /** + * Reverts the diff from a given state + * + * By default it applies the reverse event + */ + revert(state: T): void { + this.getReverse().apply(state); + } - /** - * Get the reverse event - * - * By default it returns a stub event that does nothing - */ - protected getReverse(): Diff { - return new Diff(); - } - } - - /** - * Collection of sequential diffs - */ - export class DiffLog { - private diffs: Diff[] = [] - - /** - * Add a single diff at the end of the log - */ - add(diff: Diff): void { - this.diffs.push(diff); - } - - /** - * Get the diff at a specific index - */ - get(idx: number): Diff | null { - return this.diffs[idx] || null; - } - - /** - * Return the total count of diffs - */ - count(): number { - return this.diffs.length; - } - - /** - * Clean all stored diffs, starting at a given index - * - * The caller should be sure that no log client is beyond the cut index. - */ - clear(start = 0): void { - this.diffs = this.diffs.slice(0, start); - } - } - - /** - * Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed - */ - export class DiffLogClient { - private state: T - private log: DiffLog - private cursor = -1 - private playing = false - private stopping = false - private paused = false - private timer = Timer.global - - constructor(state: T, log: DiffLog) { - this.state = state; - this.log = log; - this.cursor = log.count() - 1; - } - - /** - * Returns true if the log is currently playing - */ - isPlaying(): boolean { - return this.playing && !this.paused && !this.stopping; - } - - /** - * Get the current diff pointed at - */ - getCurrent(): Diff | null { - return this.log.get(this.cursor); - } - - /** - * Push a diff to the underlying log, applying it immediately if required - */ - add(diff: Diff, apply = true): void { - this.log.add(diff); - if (apply) { - this.jumpToEnd(); - } - } - - /** - * Apply the underlying log continuously, until *stop* is called - * - * If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming - */ - async play(after_apply?: (diff: Diff) => Promise): Promise { - if (this.playing) { - console.error("DiffLogClient already playing", this); - return; - } - - this.playing = true; - this.stopping = false; - - while (this.playing) { - if (!this.paused) { - let diff = this.forward(); - if (diff && after_apply) { - await after_apply(diff); - } - } - - if (this.atEnd()) { - if (this.stopping) { - break; - } else { - await this.timer.sleep(50); - } - } - } - } - - /** - * Stop the previous *play* - */ - stop(immediate = true): void { - if (!this.playing) { - console.error("DiffLogClient not playing", this); - return; - } - - if (immediate) { - this.playing = false; - } - this.stopping = true; - } - - /** - * Make a step backward in time (revert one diff) - */ - backward(): Diff | null { - if (!this.atStart()) { - this.cursor -= 1; - this.paused = true; - - let diff = this.log.get(this.cursor + 1); - if (diff) { - diff.revert(this.state); - } - return diff; - } else { - return null; - } - } - - /** - * Make a step forward in time (apply one diff) - */ - forward(): Diff | null { - if (!this.atEnd()) { - this.cursor += 1; - if (this.atEnd()) { - this.paused = false; - } - - let diff = this.log.get(this.cursor); - if (diff) { - diff.apply(this.state); - } - return diff; - } else { - return null; - } - } - - /** - * Jump to the start of the log - * - * This will rewind all applied event - */ - jumpToStart() { - while (!this.atStart()) { - this.backward(); - } - } - - /** - * Jump to the end of the log - * - * This will apply all remaining event - */ - jumpToEnd() { - while (!this.atEnd()) { - this.forward(); - } - } - - /** - * Check if we are currently at the start of the log - */ - atStart(): boolean { - return this.cursor < 0; - } - - /** - * Check if we are currently at the end of the log - */ - atEnd(): boolean { - return this.cursor >= this.log.count() - 1; - } - - /** - * Truncate all diffs after the current position - * - * This is useful when using the log to "undo" something - */ - truncate(): void { - this.log.clear(this.cursor + 1); - } - } + /** + * Get the reverse event + * + * By default it returns a stub event that does nothing + */ + protected getReverse(): Diff { + return new Diff(); + } +} + +/** + * Collection of sequential diffs + */ +export class DiffLog { + private diffs: Diff[] = [] + + /** + * Add a single diff at the end of the log + */ + add(diff: Diff): void { + this.diffs.push(diff); + } + + /** + * Get the diff at a specific index + */ + get(idx: number): Diff | null { + return this.diffs[idx] || null; + } + + /** + * Return the total count of diffs + */ + count(): number { + return this.diffs.length; + } + + /** + * Clean all stored diffs, starting at a given index + * + * The caller should be sure that no log client is beyond the cut index. + */ + clear(start = 0): void { + this.diffs = this.diffs.slice(0, start); + } +} + +/** + * Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed + */ +export class DiffLogClient { + private state: T + private log: DiffLog + private cursor = -1 + private playing = false + private stopping = false + private paused = false + private timer = Timer.global + + constructor(state: T, log: DiffLog) { + this.state = state; + this.log = log; + this.cursor = log.count() - 1; + } + + /** + * Returns true if the log is currently playing + */ + isPlaying(): boolean { + return this.playing && !this.paused && !this.stopping; + } + + /** + * Get the current diff pointed at + */ + getCurrent(): Diff | null { + return this.log.get(this.cursor); + } + + /** + * Push a diff to the underlying log, applying it immediately if required + */ + add(diff: Diff, apply = true): void { + this.log.add(diff); + if (apply) { + this.jumpToEnd(); + } + } + + /** + * Apply the underlying log continuously, until *stop* is called + * + * If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming + */ + async play(after_apply?: (diff: Diff) => Promise): Promise { + if (this.playing) { + console.error("DiffLogClient already playing", this); + return; + } + + this.playing = true; + this.stopping = false; + + while (this.playing) { + if (!this.paused) { + let diff = this.forward(); + if (diff && after_apply) { + await after_apply(diff); + } + } + + if (this.atEnd()) { + if (this.stopping) { + break; + } else { + await this.timer.sleep(50); + } + } + } + } + + /** + * Stop the previous *play* + */ + stop(immediate = true): void { + if (!this.playing) { + console.error("DiffLogClient not playing", this); + return; + } + + if (immediate) { + this.playing = false; + } + this.stopping = true; + } + + /** + * Make a step backward in time (revert one diff) + */ + backward(): Diff | null { + if (!this.atStart()) { + this.cursor -= 1; + this.paused = true; + + let diff = this.log.get(this.cursor + 1); + if (diff) { + diff.revert(this.state); + } + return diff; + } else { + return null; + } + } + + /** + * Make a step forward in time (apply one diff) + */ + forward(): Diff | null { + if (!this.atEnd()) { + this.cursor += 1; + if (this.atEnd()) { + this.paused = false; + } + + let diff = this.log.get(this.cursor); + if (diff) { + diff.apply(this.state); + } + return diff; + } else { + return null; + } + } + + /** + * Jump to the start of the log + * + * This will rewind all applied event + */ + jumpToStart() { + while (!this.atStart()) { + this.backward(); + } + } + + /** + * Jump to the end of the log + * + * This will apply all remaining event + */ + jumpToEnd() { + while (!this.atEnd()) { + this.forward(); + } + } + + /** + * Check if we are currently at the start of the log + */ + atStart(): boolean { + return this.cursor < 0; + } + + /** + * Check if we are currently at the end of the log + */ + atEnd(): boolean { + return this.cursor >= this.log.count() - 1; + } + + /** + * Truncate all diffs after the current position + * + * This is useful when using the log to "undo" something + */ + truncate(): void { + this.log.clear(this.cursor + 1); + } } diff --git a/src/common/Iterators.spec.ts b/src/common/Iterators.spec.ts index f9c1579..43fa134 100644 --- a/src/common/Iterators.spec.ts +++ b/src/common/Iterators.spec.ts @@ -1,241 +1,239 @@ -module TK { - testing("Iterators", test => { - function checkit(check: TestContext, base_iterator: Iterable, values: T[], infinite = false) { - function checker(check: TestContext) { - let iterator = base_iterator[Symbol.iterator](); - values.forEach((value, idx) => { - let state = iterator.next(); - check.equals(state.done, false, `index ${idx} not done`); - check.equals(state.value, value, `index ${idx} value`); - }); - if (!infinite) { - range(3).forEach(oidx => { - let state = iterator.next(); - check.equals(state.done, true, `index ${values.length + oidx} done`); - }); - } - } - - check.in("first iteration", checker); - check.in("second iteration", checker); - } - - test.case("constructs an iterator from a recurrent formula", check => { - checkit(check, irecur(1, x => x + 2), [1, 3, 5], true); - checkit(check, irecur(4, x => x ? x - 1 : null), [4, 3, 2, 1, 0]); +testing("Iterators", test => { + function checkit(check: TestContext, base_iterator: Iterable, values: T[], infinite = false) { + function checker(check: TestContext) { + let iterator = base_iterator[Symbol.iterator](); + values.forEach((value, idx) => { + let state = iterator.next(); + check.equals(state.done, false, `index ${idx} not done`); + check.equals(state.value, value, `index ${idx} value`); + }); + if (!infinite) { + range(3).forEach(oidx => { + let state = iterator.next(); + check.equals(state.done, true, `index ${values.length + oidx} done`); }); + } + } - test.case("constructs an iterator from an array", check => { - checkit(check, iarray([]), []); - checkit(check, iarray([1, 2, 3]), [1, 2, 3]); - }); + check.in("first iteration", checker); + check.in("second iteration", checker); + } - test.case("constructs an iterator from a single value", check => { - checkit(check, isingle(1), [1]); - checkit(check, isingle("a"), ["a"]); - }); + test.case("constructs an iterator from a recurrent formula", check => { + checkit(check, irecur(1, x => x + 2), [1, 3, 5], true); + checkit(check, irecur(4, x => x ? x - 1 : null), [4, 3, 2, 1, 0]); + }); - test.case("repeats a value", check => { - checkit(check, irepeat("a"), ["a", "a", "a", "a"], true); - checkit(check, irepeat("a", 3), ["a", "a", "a"]); - }); + test.case("constructs an iterator from an array", check => { + checkit(check, iarray([]), []); + checkit(check, iarray([1, 2, 3]), [1, 2, 3]); + }); - test.case("calls a function for each yielded value", check => { - let iterator = iarray([1, 2, 3]); - let result: number[] = []; - iforeach(iterator, bound(result, "push")); - check.equals(result, [1, 2, 3]); + test.case("constructs an iterator from a single value", check => { + checkit(check, isingle(1), [1]); + checkit(check, isingle("a"), ["a"]); + }); - result = []; - iforeach(iterator, i => { - result.push(i); - if (i == 2) { - return null; - } else { - return undefined; - } - }); - check.equals(result, [1, 2]); + test.case("repeats a value", check => { + checkit(check, irepeat("a"), ["a", "a", "a", "a"], true); + checkit(check, irepeat("a", 3), ["a", "a", "a"]); + }); - result = []; - iforeach(iterator, i => { - result.push(i); - return i; - }, 2); - check.equals(result, [1, 2]); - }); + test.case("calls a function for each yielded value", check => { + let iterator = iarray([1, 2, 3]); + let result: number[] = []; + iforeach(iterator, bound(result, "push")); + check.equals(result, [1, 2, 3]); - test.case("finds the first item passing a predicate", check => { - check.equals(ifirst(iarray([]), i => i % 2 == 0), null); - check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2); - check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null); - }); - - test.case("finds the first item mapping to a value", check => { - let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null - check.equals(ifirstmap(iarray([]), predicate), null); - check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8"); - check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null); - }); - - test.case("materializes an array from an iterator", check => { - check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]); - - check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize"); - }); - - test.case("creates an iterator in a range of integers", check => { - checkit(check, irange(4), [0, 1, 2, 3]); - checkit(check, irange(4, 1), [1, 2, 3, 4]); - checkit(check, irange(5, 3, 2), [3, 5, 7, 9, 11]); - checkit(check, irange(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); - }); - - test.case("uses a step iterator to scan numbers", check => { - checkit(check, istep(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); - checkit(check, istep(3), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], true); - checkit(check, istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]); - checkit(check, istep(8, IEMPTY), [8]); - checkit(check, istep(1, irange()), [1, 1, 2, 4, 7, 11, 16], true); - }); - - test.case("skips a number of values", check => { - checkit(check, iskip(irange(7), 3), [3, 4, 5, 6]); - checkit(check, iskip(irange(7), 12), []); - checkit(check, iskip(IEMPTY, 3), []); - }); - - test.case("gets a value at an iterator position", check => { - check.equals(iat(irange(), -1), null); - check.equals(iat(irange(), 0), 0); - check.equals(iat(irange(), 8), 8); - check.equals(iat(irange(5), 8), null); - check.equals(iat(IEMPTY, 0), null); - }); - - test.case("chains iterator of iterators", check => { - checkit(check, ichainit(IEMPTY), []); - checkit(check, ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]); - }); - - test.case("chains iterators", check => { - checkit(check, ichain(), []); - checkit(check, ichain(irange(3)), [0, 1, 2]); - checkit(check, ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]); - }); - - test.case("loops an iterator", check => { - checkit(check, iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]); - checkit(check, iloop(irange(1)), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], true); - - let onloop = check.mockfunc("onloop"); - let iterator = iloop(irange(2), 3, onloop.func)[Symbol.iterator](); - check.in("idx 0", check => { - check.equals(iterator.next().value, 0); - check.called(onloop, 0); - }); - check.in("idx 1", check => { - check.equals(iterator.next().value, 1); - check.called(onloop, 0); - }); - check.in("idx 2", check => { - check.equals(iterator.next().value, 0); - check.called(onloop, 1); - }); - check.in("idx 3", check => { - check.equals(iterator.next().value, 1); - check.called(onloop, 0); - }); - check.in("idx 4", check => { - check.equals(iterator.next().value, 0); - check.called(onloop, 1); - }); - check.in("idx 5", check => { - check.equals(iterator.next().value, 1); - check.called(onloop, 0); - }); - check.in("idx 6", check => { - check.equals(iterator.next().value, undefined); - check.called(onloop, 0); - }); - }); - - test.case("maps an iterator", check => { - checkit(check, imap(IEMPTY, i => i * 2), []); - checkit(check, imap(irange(3), i => i * 2), [0, 2, 4]); - }); - - test.case("reduces an iterator", check => { - check.equals(ireduce(IEMPTY, (a, b) => a + b, 2), 2); - check.equals(ireduce([9], (a, b) => a + b, 2), 11); - check.equals(ireduce([9, 1], (a, b) => a + b, 2), 12); - }); - - test.case("filters an iterator with a predicate", check => { - checkit(check, imap(IEMPTY, i => i % 3 == 0), []); - checkit(check, ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]); - }); - - test.case("filters an iterator with a type guard", check => { - let result = ifiltertype(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number"); - checkit(check, result, [1, 2]); - }); - - test.case("filters an iterator with a class type", check => { - let o1 = new RObject(); - let o2 = new RObject(); - let o3 = new RObjectContainer(); - let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], RObject); - checkit(check, result, [o1, o2]); - }); - - test.case("combines iterators", check => { - let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"])); - checkit(check, iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]); - }); - - test.case("zips iterators", check => { - checkit(check, izip(IEMPTY, IEMPTY), []); - checkit(check, izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]); - - checkit(check, izipg(IEMPTY, IEMPTY), []); - checkit(check, izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | undefined, string | undefined][]>[[1, "a"], [2, "b"], [3, undefined]]); - }); - - test.case("partitions iterators", check => { - let [it1, it2] = ipartition(IEMPTY, () => true); - checkit(check, it1, []); - checkit(check, it2, []); - - [it1, it2] = ipartition(irange(5), i => i % 2 == 0); - checkit(check, it1, [0, 2, 4]); - checkit(check, it2, [1, 3]); - }); - - test.case("alternatively pick from several iterables", check => { - checkit(check, ialternate([]), []); - checkit(check, ialternate([[1, 2, 3, 4], [], iarray([5, 6]), IEMPTY, iarray([7, 8, 9])]), [1, 5, 7, 2, 6, 8, 3, 9, 4]); - }); - - test.case("returns unique items", check => { - checkit(check, iunique(IEMPTY), []); - checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]); - checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]); - check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator"); - }); - - test.case("uses ireduce for some common functions", check => { - check.equals(isum(IEMPTY), 0); - check.equals(isum(irange(4)), 6); - - check.equals(icat(IEMPTY), ""); - check.equals(icat(iarray(["a", "bc", "d"])), "abcd"); - - check.equals(imin(IEMPTY), Infinity); - check.equals(imin(iarray([3, 8, 2, 4])), 2); - - check.equals(imax(IEMPTY), -Infinity); - check.equals(imax(iarray([3, 8, 2, 4])), 8); - }); + result = []; + iforeach(iterator, i => { + result.push(i); + if (i == 2) { + return null; + } else { + return undefined; + } }); -} \ No newline at end of file + check.equals(result, [1, 2]); + + result = []; + iforeach(iterator, i => { + result.push(i); + return i; + }, 2); + check.equals(result, [1, 2]); + }); + + test.case("finds the first item passing a predicate", check => { + check.equals(ifirst(iarray([]), i => i % 2 == 0), null); + check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2); + check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null); + }); + + test.case("finds the first item mapping to a value", check => { + let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null + check.equals(ifirstmap(iarray([]), predicate), null); + check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8"); + check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null); + }); + + test.case("materializes an array from an iterator", check => { + check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]); + + check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize"); + }); + + test.case("creates an iterator in a range of integers", check => { + checkit(check, irange(4), [0, 1, 2, 3]); + checkit(check, irange(4, 1), [1, 2, 3, 4]); + checkit(check, irange(5, 3, 2), [3, 5, 7, 9, 11]); + checkit(check, irange(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); + }); + + test.case("uses a step iterator to scan numbers", check => { + checkit(check, istep(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); + checkit(check, istep(3), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], true); + checkit(check, istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]); + checkit(check, istep(8, IEMPTY), [8]); + checkit(check, istep(1, irange()), [1, 1, 2, 4, 7, 11, 16], true); + }); + + test.case("skips a number of values", check => { + checkit(check, iskip(irange(7), 3), [3, 4, 5, 6]); + checkit(check, iskip(irange(7), 12), []); + checkit(check, iskip(IEMPTY, 3), []); + }); + + test.case("gets a value at an iterator position", check => { + check.equals(iat(irange(), -1), null); + check.equals(iat(irange(), 0), 0); + check.equals(iat(irange(), 8), 8); + check.equals(iat(irange(5), 8), null); + check.equals(iat(IEMPTY, 0), null); + }); + + test.case("chains iterator of iterators", check => { + checkit(check, ichainit(IEMPTY), []); + checkit(check, ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]); + }); + + test.case("chains iterators", check => { + checkit(check, ichain(), []); + checkit(check, ichain(irange(3)), [0, 1, 2]); + checkit(check, ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]); + }); + + test.case("loops an iterator", check => { + checkit(check, iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]); + checkit(check, iloop(irange(1)), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], true); + + let onloop = check.mockfunc("onloop"); + let iterator = iloop(irange(2), 3, onloop.func)[Symbol.iterator](); + check.in("idx 0", check => { + check.equals(iterator.next().value, 0); + check.called(onloop, 0); + }); + check.in("idx 1", check => { + check.equals(iterator.next().value, 1); + check.called(onloop, 0); + }); + check.in("idx 2", check => { + check.equals(iterator.next().value, 0); + check.called(onloop, 1); + }); + check.in("idx 3", check => { + check.equals(iterator.next().value, 1); + check.called(onloop, 0); + }); + check.in("idx 4", check => { + check.equals(iterator.next().value, 0); + check.called(onloop, 1); + }); + check.in("idx 5", check => { + check.equals(iterator.next().value, 1); + check.called(onloop, 0); + }); + check.in("idx 6", check => { + check.equals(iterator.next().value, undefined); + check.called(onloop, 0); + }); + }); + + test.case("maps an iterator", check => { + checkit(check, imap(IEMPTY, i => i * 2), []); + checkit(check, imap(irange(3), i => i * 2), [0, 2, 4]); + }); + + test.case("reduces an iterator", check => { + check.equals(ireduce(IEMPTY, (a, b) => a + b, 2), 2); + check.equals(ireduce([9], (a, b) => a + b, 2), 11); + check.equals(ireduce([9, 1], (a, b) => a + b, 2), 12); + }); + + test.case("filters an iterator with a predicate", check => { + checkit(check, imap(IEMPTY, i => i % 3 == 0), []); + checkit(check, ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]); + }); + + test.case("filters an iterator with a type guard", check => { + let result = ifiltertype(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number"); + checkit(check, result, [1, 2]); + }); + + test.case("filters an iterator with a class type", check => { + let o1 = new RObject(); + let o2 = new RObject(); + let o3 = new RObjectContainer(); + let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], RObject); + checkit(check, result, [o1, o2]); + }); + + test.case("combines iterators", check => { + let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"])); + checkit(check, iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]); + }); + + test.case("zips iterators", check => { + checkit(check, izip(IEMPTY, IEMPTY), []); + checkit(check, izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]); + + checkit(check, izipg(IEMPTY, IEMPTY), []); + checkit(check, izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | undefined, string | undefined][]>[[1, "a"], [2, "b"], [3, undefined]]); + }); + + test.case("partitions iterators", check => { + let [it1, it2] = ipartition(IEMPTY, () => true); + checkit(check, it1, []); + checkit(check, it2, []); + + [it1, it2] = ipartition(irange(5), i => i % 2 == 0); + checkit(check, it1, [0, 2, 4]); + checkit(check, it2, [1, 3]); + }); + + test.case("alternatively pick from several iterables", check => { + checkit(check, ialternate([]), []); + checkit(check, ialternate([[1, 2, 3, 4], [], iarray([5, 6]), IEMPTY, iarray([7, 8, 9])]), [1, 5, 7, 2, 6, 8, 3, 9, 4]); + }); + + test.case("returns unique items", check => { + checkit(check, iunique(IEMPTY), []); + checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]); + checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]); + check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator"); + }); + + test.case("uses ireduce for some common functions", check => { + check.equals(isum(IEMPTY), 0); + check.equals(isum(irange(4)), 6); + + check.equals(icat(IEMPTY), ""); + check.equals(icat(iarray(["a", "bc", "d"])), "abcd"); + + check.equals(imin(IEMPTY), Infinity); + check.equals(imin(iarray([3, 8, 2, 4])), 2); + + check.equals(imax(IEMPTY), -Infinity); + check.equals(imax(iarray([3, 8, 2, 4])), 8); + }); +}); diff --git a/src/common/Iterators.ts b/src/common/Iterators.ts index 1f78373..0a2bcad 100644 --- a/src/common/Iterators.ts +++ b/src/common/Iterators.ts @@ -1,436 +1,426 @@ +import { contains } from "./Tools"; + /** - * Lazy iterators to work on dynamic data sets without materializing them. - * - * They allow to work on infinite streams of values, with limited memory consumption. - * - * Functions in this file that do not return an Iterable are "materializing", meaning that they - * may consume iterators up to the end, and will not work well on infinite iterators. - * - * These iterators are guaranteed to be repeatable, meaning that calling Symbol.iterator on them will start over. + * Empty iterator */ -module TK { - /** - * Empty iterator - */ - export const IATEND: Iterator = { - next: function () { - return { done: true, value: undefined }; - } - } - - /** - * Empty iterable - */ - export const IEMPTY: Iterable = { - [Symbol.iterator]: () => IATEND - } - - /** - * Iterable constructor, from an initial value, and a step value - */ - export function irecur(start: T, step: (a: T) => T | null): Iterable { - return { - [Symbol.iterator]: function* () { - let val: T | null = start; - do { - yield val; - val = step(val); - } while (val !== null); - } - } - } - - /** - * Iterable constructor, from an array - * - * The iterator will yield the next value each time it is called, then undefined when the array's end is reached. - */ - export function iarray(array: T[], offset = 0): Iterable { - return { - [Symbol.iterator]: function () { - return array.slice(offset)[Symbol.iterator](); - } - } - } - - /** - * Iterable constructor, from a single value - * - * The value will be yielded only once, not repeated over. - */ - export function isingle(value: T): Iterable { - return iarray([value]); - } - - /** - * Iterable that repeats the same value. - */ - export function irepeat(value: T, count = -1): Iterable { - return { - [Symbol.iterator]: function* () { - let n = count; - while (n != 0) { - yield value; - n--; - } - } - } - } - - /** - * Equivalent of Array.forEach for all iterables. - * - * If the callback returns *stopper*, the iteration is stopped. - */ - export function iforeach(iterable: Iterable, callback: (_: T) => any, stopper: any = null): void { - for (let value of iterable) { - if (callback(value) === stopper) { - break; - } - } - } - - /** - * Returns the first item passing a predicate - */ - export function ifirst(iterable: Iterable, predicate: (item: T) => boolean): T | null { - for (let value of iterable) { - if (predicate(value)) { - return value; - } - } - return null; - } - - /** - * Returns the first non-null result of a value-yielding predicate, applied to each iterator element - */ - export function ifirstmap(iterable: Iterable, predicate: (item: T1) => T2 | null): T2 | null { - for (let value of iterable) { - let res = predicate(value); - if (res !== null) { - return res; - } - } - return null; - } - - /** - * Materialize an array from consuming an iterable - * - * To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an - * exception is thrown when this limit is reached. - */ - export function imaterialize(iterable: Iterable, limit = 1000000): T[] { - let result: T[] = []; - - for (let value of iterable) { - result.push(value); - if (result.length >= limit) { - throw new Error("Length limit on iterator materialize"); - } - } - - return result; - } - - /** - * Iterate over natural integers - * - * If *count* is not specified, the iterator is infinite - */ - export function irange(count: number = -1, start = 0, step = 1): Iterable { - return { - [Symbol.iterator]: function* () { - let i = start; - let n = count; - while (n != 0) { - yield i; - i += step; - n--; - } - } - } - } - - /** - * Iterate over numbers, by applying a step taken from an other iterator - * - * This iterator stops when the "step iterator" stops - * - * With no argument, istep() == irange() - */ - export function istep(start = 0, step_iterable = irepeat(1)): Iterable { - return { - [Symbol.iterator]: function* () { - let i = start; - yield i; - for (let step of step_iterable) { - i += step; - yield i; - } - } - } - } - - /** - * Skip a given number of values from an iterator, discarding them. - */ - export function iskip(iterable: Iterable, count = 1): Iterable { - return { - [Symbol.iterator]: function () { - let iterator = iterable[Symbol.iterator](); - let n = count; - while (n-- > 0) { - iterator.next(); - } - return iterator; - } - } - } - - /** - * Return the value at a given position in the iterator - */ - export function iat(iterable: Iterable, position: number): T | null { - if (position < 0) { - return null; - } else { - if (position > 0) { - iterable = iskip(iterable, position); - } - let iterator = iterable[Symbol.iterator](); - let state = iterator.next(); - return state.done ? null : state.value; - } - } - - /** - * Chain an iterable of iterables. - * - * This will yield values from the first yielded iterator, then the second one, and so on... - */ - export function ichainit(iterables: Iterable>): Iterable { - return { - [Symbol.iterator]: function* () { - for (let iterable of iterables) { - for (let value of iterable) { - yield value; - } - } - } - } - } - - /** - * Chain iterables. - * - * This will yield values from the first iterator, then the second one, and so on... - */ - export function ichain(...iterables: Iterable[]): Iterable { - if (iterables.length == 0) { - return IEMPTY; - } else { - return ichainit(iterables); - } - } - - /** - * Loop an iterator for a number of times. - * - * If count is negative, if will loop forever (infinite iterator). - * - * onloop may be used to know when the iterator resets. - */ - export function iloop(base: Iterable, count = -1, onloop?: Function): Iterable { - return { - [Symbol.iterator]: function* () { - let n = count; - let start = false; - while (n-- != 0) { - for (let value of base) { - if (start) { - if (onloop) { - onloop(); - } - start = false; - } - yield value; - } - start = true; - } - } - } - } - - /** - * Iterator version of "map". - */ - export function imap(iterable: Iterable, mapfunc: (_: T1) => T2): Iterable { - return { - [Symbol.iterator]: function* () { - for (let value of iterable) { - yield mapfunc(value); - } - } - } - } - - /** - * Iterator version of "reduce". - */ - export function ireduce(iterable: Iterable, reduce: (item1: T, item2: T) => T, init: T): T { - let result = init; - for (let value of iterable) { - result = reduce(result, value); - } - return result; - } - - /** - * Iterator version of "filter". - */ - export function ifilter(iterable: Iterable, filterfunc: (_: T) => boolean): Iterable { - return { - [Symbol.iterator]: function* () { - for (let value of iterable) { - if (filterfunc(value)) { - yield value; - } - } - } - } - } - - /** - * Type filter, to return a list of instances of a given type - */ - export function ifiltertype(iterable: Iterable, filter: (item: any) => item is T): Iterable { - return ifilter(iterable, filter); - } - - /** - * Class filter, to return a list of instances of a given type - */ - export function ifilterclass(iterable: Iterable, classref: { new(...args: any[]): T }): Iterable { - return ifilter(iterable, (item): item is T => item instanceof classref); - } - - /** - * Combine two iterables. - * - * This iterates through the second one several times, so if one iterator may be infinite, - * it should be the first one. - */ - export function icombine(it1: Iterable, it2: Iterable): Iterable<[T1, T2]> { - return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2]))); - } - - /** - * Advance through two iterables at the same time, yielding item pairs - * - * Iteration will stop at the first of the two iterators that stops. - */ - export function izip(it1: Iterable, it2: Iterable): Iterable<[T1, T2]> { - return { - [Symbol.iterator]: function* () { - let iterator1 = it1[Symbol.iterator](); - let iterator2 = it2[Symbol.iterator](); - let state1 = iterator1.next(); - let state2 = iterator2.next(); - while (!state1.done && !state2.done) { - yield [state1.value, state2.value]; - state1 = iterator1.next(); - state2 = iterator2.next(); - } - } - } - } - - /** - * Advance two iterables at the same time, yielding item pairs (greedy version) - * - * Iteration will stop when both iterators are consumed, returning partial couples (undefined in the peer) if needed. - */ - export function izipg(it1: Iterable, it2: Iterable): Iterable<[T1 | undefined, T2 | undefined]> { - return { - [Symbol.iterator]: function* () { - let iterator1 = it1[Symbol.iterator](); - let iterator2 = it2[Symbol.iterator](); - let state1 = iterator1.next(); - let state2 = iterator2.next(); - while (!state1.done || !state2.done) { - yield [state1.value, state2.value]; - state1 = iterator1.next(); - state2 = iterator2.next(); - } - } - } - } - - /** - * Partition in two iterables, one with values that pass the predicate, the other with values that don't - */ - export function ipartition(iterable: Iterable, predicate: (item: T) => boolean): [Iterable, Iterable] { - return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))]; - } - - /** - * Alternate between several iterables (pick one from the first one, then one from the second...) - */ - export function ialternate(iterables: Iterable[]): Iterable { - return { - [Symbol.iterator]: function* () { - let iterators = iterables.map(iterable => iterable[Symbol.iterator]()); - let done: boolean; - do { - done = false; - // TODO Remove "dried-out" iterators - for (let iterator of iterators) { - let state = iterator.next(); - if (!state.done) { - done = true; - yield state.value; - } - } - } while (done); - } - } - } - - /** - * Yield items from an iterator only once. - * - * Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on - * infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown - * when this limit is reached). - * - * This function is O(n²) - */ - export function iunique(iterable: Iterable, limit = 1000000): Iterable { - return { - [Symbol.iterator]: function* () { - let done: T[] = []; - let n = limit; - for (let value of iterable) { - if (!contains(done, value)) { - if (n-- > 0) { - done.push(value); - yield value; - } else { - throw new Error("Unique count limit on iterator"); - } - } - } - } - } - } - - /** - * Common reduce shortcuts - */ - export const isum = (iterable: Iterable) => ireduce(iterable, (a, b) => a + b, 0); - export const icat = (iterable: Iterable) => ireduce(iterable, (a, b) => a + b, ""); - export const imin = (iterable: Iterable) => ireduce(iterable, Math.min, Infinity); - export const imax = (iterable: Iterable) => ireduce(iterable, Math.max, -Infinity); +export const IATEND: Iterator = { + next: function () { + return { done: true, value: undefined }; + } } + +/** + * Empty iterable + */ +export const IEMPTY: Iterable = { + [Symbol.iterator]: () => IATEND +} + +/** + * Iterable constructor, from an initial value, and a step value + */ +export function irecur(start: T, step: (a: T) => T | null): Iterable { + return { + [Symbol.iterator]: function* () { + let val: T | null = start; + do { + yield val; + val = step(val); + } while (val !== null); + } + } +} + +/** + * Iterable constructor, from an array + * + * The iterator will yield the next value each time it is called, then undefined when the array's end is reached. + */ +export function iarray(array: T[], offset = 0): Iterable { + return { + [Symbol.iterator]: function () { + return array.slice(offset)[Symbol.iterator](); + } + } +} + +/** + * Iterable constructor, from a single value + * + * The value will be yielded only once, not repeated over. + */ +export function isingle(value: T): Iterable { + return iarray([value]); +} + +/** + * Iterable that repeats the same value. + */ +export function irepeat(value: T, count = -1): Iterable { + return { + [Symbol.iterator]: function* () { + let n = count; + while (n != 0) { + yield value; + n--; + } + } + } +} + +/** + * Equivalent of Array.forEach for all iterables. + * + * If the callback returns *stopper*, the iteration is stopped. + */ +export function iforeach(iterable: Iterable, callback: (_: T) => any, stopper: any = null): void { + for (let value of iterable) { + if (callback(value) === stopper) { + break; + } + } +} + +/** + * Returns the first item passing a predicate + */ +export function ifirst(iterable: Iterable, predicate: (item: T) => boolean): T | null { + for (let value of iterable) { + if (predicate(value)) { + return value; + } + } + return null; +} + +/** + * Returns the first non-null result of a value-yielding predicate, applied to each iterator element + */ +export function ifirstmap(iterable: Iterable, predicate: (item: T1) => T2 | null): T2 | null { + for (let value of iterable) { + let res = predicate(value); + if (res !== null) { + return res; + } + } + return null; +} + +/** + * Materialize an array from consuming an iterable + * + * To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an + * exception is thrown when this limit is reached. + */ +export function imaterialize(iterable: Iterable, limit = 1000000): T[] { + let result: T[] = []; + + for (let value of iterable) { + result.push(value); + if (result.length >= limit) { + throw new Error("Length limit on iterator materialize"); + } + } + + return result; +} + +/** + * Iterate over natural integers + * + * If *count* is not specified, the iterator is infinite + */ +export function irange(count: number = -1, start = 0, step = 1): Iterable { + return { + [Symbol.iterator]: function* () { + let i = start; + let n = count; + while (n != 0) { + yield i; + i += step; + n--; + } + } + } +} + +/** + * Iterate over numbers, by applying a step taken from an other iterator + * + * This iterator stops when the "step iterator" stops + * + * With no argument, istep() == irange() + */ +export function istep(start = 0, step_iterable = irepeat(1)): Iterable { + return { + [Symbol.iterator]: function* () { + let i = start; + yield i; + for (let step of step_iterable) { + i += step; + yield i; + } + } + } +} + +/** + * Skip a given number of values from an iterator, discarding them. + */ +export function iskip(iterable: Iterable, count = 1): Iterable { + return { + [Symbol.iterator]: function () { + let iterator = iterable[Symbol.iterator](); + let n = count; + while (n-- > 0) { + iterator.next(); + } + return iterator; + } + } +} + +/** + * Return the value at a given position in the iterator + */ +export function iat(iterable: Iterable, position: number): T | null { + if (position < 0) { + return null; + } else { + if (position > 0) { + iterable = iskip(iterable, position); + } + let iterator = iterable[Symbol.iterator](); + let state = iterator.next(); + return state.done ? null : state.value; + } +} + +/** + * Chain an iterable of iterables. + * + * This will yield values from the first yielded iterator, then the second one, and so on... + */ +export function ichainit(iterables: Iterable>): Iterable { + return { + [Symbol.iterator]: function* () { + for (let iterable of iterables) { + for (let value of iterable) { + yield value; + } + } + } + } +} + +/** + * Chain iterables. + * + * This will yield values from the first iterator, then the second one, and so on... + */ +export function ichain(...iterables: Iterable[]): Iterable { + if (iterables.length == 0) { + return IEMPTY; + } else { + return ichainit(iterables); + } +} + +/** + * Loop an iterator for a number of times. + * + * If count is negative, if will loop forever (infinite iterator). + * + * onloop may be used to know when the iterator resets. + */ +export function iloop(base: Iterable, count = -1, onloop?: Function): Iterable { + return { + [Symbol.iterator]: function* () { + let n = count; + let start = false; + while (n-- != 0) { + for (let value of base) { + if (start) { + if (onloop) { + onloop(); + } + start = false; + } + yield value; + } + start = true; + } + } + } +} + +/** + * Iterator version of "map". + */ +export function imap(iterable: Iterable, mapfunc: (_: T1) => T2): Iterable { + return { + [Symbol.iterator]: function* () { + for (let value of iterable) { + yield mapfunc(value); + } + } + } +} + +/** + * Iterator version of "reduce". + */ +export function ireduce(iterable: Iterable, reduce: (item1: T, item2: T) => T, init: T): T { + let result = init; + for (let value of iterable) { + result = reduce(result, value); + } + return result; +} + +/** + * Iterator version of "filter". + */ +export function ifilter(iterable: Iterable, filterfunc: (_: T) => boolean): Iterable { + return { + [Symbol.iterator]: function* () { + for (let value of iterable) { + if (filterfunc(value)) { + yield value; + } + } + } + } +} + +/** + * Type filter, to return a list of instances of a given type + */ +export function ifiltertype(iterable: Iterable, filter: (item: any) => item is T): Iterable { + return ifilter(iterable, filter); +} + +/** + * Class filter, to return a list of instances of a given type + */ +export function ifilterclass(iterable: Iterable, classref: { new(...args: any[]): T }): Iterable { + return ifilter(iterable, (item): item is T => item instanceof classref); +} + +/** + * Combine two iterables. + * + * This iterates through the second one several times, so if one iterator may be infinite, + * it should be the first one. + */ +export function icombine(it1: Iterable, it2: Iterable): Iterable<[T1, T2]> { + return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2]))); +} + +/** + * Advance through two iterables at the same time, yielding item pairs + * + * Iteration will stop at the first of the two iterators that stops. + */ +export function izip(it1: Iterable, it2: Iterable): Iterable<[T1, T2]> { + return { + [Symbol.iterator]: function* () { + let iterator1 = it1[Symbol.iterator](); + let iterator2 = it2[Symbol.iterator](); + let state1 = iterator1.next(); + let state2 = iterator2.next(); + while (!state1.done && !state2.done) { + yield [state1.value, state2.value]; + state1 = iterator1.next(); + state2 = iterator2.next(); + } + } + } +} + +/** + * Advance two iterables at the same time, yielding item pairs (greedy version) + * + * Iteration will stop when both iterators are consumed, returning partial couples (undefined in the peer) if needed. + */ +export function izipg(it1: Iterable, it2: Iterable): Iterable<[T1 | undefined, T2 | undefined]> { + return { + [Symbol.iterator]: function* () { + let iterator1 = it1[Symbol.iterator](); + let iterator2 = it2[Symbol.iterator](); + let state1 = iterator1.next(); + let state2 = iterator2.next(); + while (!state1.done || !state2.done) { + yield [state1.value, state2.value]; + state1 = iterator1.next(); + state2 = iterator2.next(); + } + } + } +} + +/** + * Partition in two iterables, one with values that pass the predicate, the other with values that don't + */ +export function ipartition(iterable: Iterable, predicate: (item: T) => boolean): [Iterable, Iterable] { + return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))]; +} + +/** + * Alternate between several iterables (pick one from the first one, then one from the second...) + */ +export function ialternate(iterables: Iterable[]): Iterable { + return { + [Symbol.iterator]: function* () { + let iterators = iterables.map(iterable => iterable[Symbol.iterator]()); + let done: boolean; + do { + done = false; + // TODO Remove "dried-out" iterators + for (let iterator of iterators) { + let state = iterator.next(); + if (!state.done) { + done = true; + yield state.value; + } + } + } while (done); + } + } +} + +/** + * Yield items from an iterator only once. + * + * Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on + * infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown + * when this limit is reached). + * + * This function is O(n²) + */ +export function iunique(iterable: Iterable, limit = 1000000): Iterable { + return { + [Symbol.iterator]: function* () { + let done: T[] = []; + let n = limit; + for (let value of iterable) { + if (!contains(done, value)) { + if (n-- > 0) { + done.push(value); + yield value; + } else { + throw new Error("Unique count limit on iterator"); + } + } + } + } + } +} + +/** + * Common reduce shortcuts + */ +export const isum = (iterable: Iterable) => ireduce(iterable, (a, b) => a + b, 0); +export const icat = (iterable: Iterable) => ireduce(iterable, (a, b) => a + b, ""); +export const imin = (iterable: Iterable) => ireduce(iterable, Math.min, Infinity); +export const imax = (iterable: Iterable) => ireduce(iterable, Math.max, -Infinity); diff --git a/src/common/RObject.spec.ts b/src/common/RObject.spec.ts index 199f613..e2f4815 100644 --- a/src/common/RObject.spec.ts +++ b/src/common/RObject.spec.ts @@ -1,125 +1,123 @@ -module TK.Specs { - export class TestRObject extends RObject { - x: number - constructor(x = RandomGenerator.global.random()) { - super(); - this.x = x; - } - }; +export class TestRObject extends RObject { + x: number + constructor(x = RandomGenerator.global.random()) { + super(); + this.x = x; + } +}; - testing("RObject", test => { - test.setup(function () { - (RObject)._next_id = 0; - }) +testing("RObject", test => { + test.setup(function () { + (RObject)._next_id = 0; + }) - test.case("gets a sequential id", check => { - let o1 = new TestRObject(); - check.equals(o1.id, 0); - let o2 = new TestRObject(); - let o3 = new TestRObject(); - check.equals(o2.id, 1); - check.equals(o3.id, 2); + test.case("gets a sequential id", check => { + let o1 = new TestRObject(); + check.equals(o1.id, 0); + let o2 = new TestRObject(); + let o3 = new TestRObject(); + check.equals(o2.id, 1); + check.equals(o3.id, 2); - check.equals(rid(o3), 2); - check.equals(rid(o3.id), 2); - }) + check.equals(rid(o3), 2); + check.equals(rid(o3.id), 2); + }) - test.case("checks object identity", check => { - let o1 = new TestRObject(1); - let o2 = new TestRObject(1); - let o3 = duplicate(o1, TK.Specs); + test.case("checks object identity", check => { + let o1 = new TestRObject(1); + let o2 = new TestRObject(1); + let o3 = duplicate(o1, TK.Specs); - check.equals(o1.is(o1), true, "o1 is o1"); - check.equals(o1.is(o2), false, "o1 is not o2"); - check.equals(o1.is(o3), true, "o1 is o3"); - check.equals(o1.is(null), false, "o1 is not null"); + check.equals(o1.is(o1), true, "o1 is o1"); + check.equals(o1.is(o2), false, "o1 is not o2"); + check.equals(o1.is(o3), true, "o1 is o3"); + check.equals(o1.is(null), false, "o1 is not null"); - check.equals(o2.is(o1), false, "o2 is not o1"); - check.equals(o2.is(o2), true, "o2 is o2"); - check.equals(o2.is(o3), false, "o2 is not o3"); - check.equals(o2.is(null), false, "o2 is not null"); + check.equals(o2.is(o1), false, "o2 is not o1"); + check.equals(o2.is(o2), true, "o2 is o2"); + check.equals(o2.is(o3), false, "o2 is not o3"); + check.equals(o2.is(null), false, "o2 is not null"); - check.equals(o3.is(o1), true, "o3 is o1"); - check.equals(o3.is(o2), false, "o3 is not o2"); - check.equals(o3.is(o3), true, "o3 is o3"); - check.equals(o3.is(null), false, "o3 is not null"); - }) + check.equals(o3.is(o1), true, "o3 is o1"); + check.equals(o3.is(o2), false, "o3 is not o2"); + check.equals(o3.is(o3), true, "o3 is o3"); + check.equals(o3.is(null), false, "o3 is not null"); + }) - test.case("resets global id on unserialize", check => { - let o1 = new TestRObject(); - check.equals(o1.id, 0); - let o2 = new TestRObject(); - check.equals(o2.id, 1); + test.case("resets global id on unserialize", check => { + let o1 = new TestRObject(); + check.equals(o1.id, 0); + let o2 = new TestRObject(); + check.equals(o2.id, 1); - let serializer = new Serializer(TK.Specs); - let packed = serializer.serialize({ objs: [o1, o2] }); + let serializer = new Serializer(TK.Specs); + let packed = serializer.serialize({ objs: [o1, o2] }); - (RObject)._next_id = 0; + (RObject)._next_id = 0; - check.equals(new TestRObject().id, 0); - let unpacked = serializer.unserialize(packed); - check.equals(unpacked, { objs: [o1, o2] }); - check.equals(new TestRObject().id, 2); - serializer.unserialize(packed); - check.equals(new TestRObject().id, 3); - }) - }) + check.equals(new TestRObject().id, 0); + let unpacked = serializer.unserialize(packed); + check.equals(unpacked, { objs: [o1, o2] }); + check.equals(new TestRObject().id, 2); + serializer.unserialize(packed); + check.equals(new TestRObject().id, 3); + }) +}) - testing("RObjectContainer", test => { - test.case("stored objects and get them by their id", check => { - let o1 = new TestRObject(); - let store = new RObjectContainer([o1]); +testing("RObjectContainer", test => { + test.case("stored objects and get them by their id", check => { + let o1 = new TestRObject(); + let store = new RObjectContainer([o1]); - let o2 = new TestRObject(); - check.same(store.get(o1.id), o1); - check.equals(store.get(o2.id), null); + let o2 = new TestRObject(); + check.same(store.get(o1.id), o1); + check.equals(store.get(o2.id), null); - store.add(o2); - check.same(store.get(o1.id), o1); - check.same(store.get(o2.id), o2); - }) + store.add(o2); + check.same(store.get(o1.id), o1); + check.same(store.get(o2.id), o2); + }) - test.case("lists contained objects", check => { - let store = new RObjectContainer(); - let o1 = store.add(new TestRObject()); - let o2 = store.add(new TestRObject()); + test.case("lists contained objects", check => { + let store = new RObjectContainer(); + let o1 = store.add(new TestRObject()); + let o2 = store.add(new TestRObject()); - check.equals(store.count(), 2, "count=2"); + check.equals(store.count(), 2, "count=2"); - let objects = store.list(); - check.equals(objects.length, 2, "list length=2"); - check.contains(objects, o1, "list contains o1"); - check.contains(objects, o2, "list contains o2"); + let objects = store.list(); + check.equals(objects.length, 2, "list length=2"); + check.contains(objects, o1, "list contains o1"); + check.contains(objects, o2, "list contains o2"); - objects = imaterialize(store.iterator()); - check.equals(objects.length, 2, "list length=2"); - check.contains(objects, o1, "list contains o1"); - check.contains(objects, o2, "list contains o2"); + objects = imaterialize(store.iterator()); + check.equals(objects.length, 2, "list length=2"); + check.contains(objects, o1, "list contains o1"); + check.contains(objects, o2, "list contains o2"); - let ids = store.ids(); - check.equals(ids.length, 2, "ids length=2"); - check.contains(ids, o1.id, "list contains o1.id"); - check.contains(ids, o2.id, "list contains o2.id"); - }) + let ids = store.ids(); + check.equals(ids.length, 2, "ids length=2"); + check.contains(ids, o1.id, "list contains o1.id"); + check.contains(ids, o2.id, "list contains o2.id"); + }) - test.case("removes objects", check => { - let store = new RObjectContainer(); - let o1 = store.add(new TestRObject()); - let o2 = store.add(new TestRObject()); + test.case("removes objects", check => { + let store = new RObjectContainer(); + let o1 = store.add(new TestRObject()); + let o2 = store.add(new TestRObject()); - check.in("initial", check => { - check.equals(store.count(), 2, "count=2"); - check.same(store.get(o1.id), o1, "o1 present"); - check.same(store.get(o2.id), o2, "o2 present"); - }); + check.in("initial", check => { + check.equals(store.count(), 2, "count=2"); + check.same(store.get(o1.id), o1, "o1 present"); + check.same(store.get(o2.id), o2, "o2 present"); + }); - store.remove(o1); + store.remove(o1); - check.in("removed o1", check => { - check.equals(store.count(), 1, "count=1"); - check.same(store.get(o1.id), null, "o1 missing"); - check.same(store.get(o2.id), o2, "o2 present"); - }); - }) - }) -} \ No newline at end of file + check.in("removed o1", check => { + check.equals(store.count(), 1, "count=1"); + check.same(store.get(o1.id), null, "o1 missing"); + check.same(store.get(o2.id), o2, "o2 present"); + }); + }) +}) diff --git a/src/common/RObject.ts b/src/common/RObject.ts index 4c39585..bb6620e 100644 --- a/src/common/RObject.ts +++ b/src/common/RObject.ts @@ -1,100 +1,101 @@ -module TK { - export type RObjectId = number +import { iarray } from "./Iterators"; +import { values } from "./Tools"; - /** - * Returns the id of an object - */ - export function rid(obj: RObject | RObjectId): number { - return (obj instanceof RObject) ? obj.id : obj; - } +export type RObjectId = number - /** - * Referenced objects, with unique ID. - * - * Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer. - */ - export class RObject { - readonly id: RObjectId = RObject._next_id++ - private static _next_id = 0 - - postUnserialize() { - if (this.id >= RObject._next_id) { - RObject._next_id = this.id + 1; - } - } - - /** - * Check that two objects are the same (only by comparing their ID) - */ - is(other: RObject | RObjectId | null): boolean { - if (other === null) { - return false; - } else if (other instanceof RObject) { - return this.id === other.id; - } else { - return this.id === other; - } - } - } - - /** - * Container to track referenced objects - */ - export class RObjectContainer { - private objects: { [index: number]: T } = {} - - constructor(objects: T[] = []) { - objects.forEach(obj => this.add(obj)); - } - - /** - * Add an object to the container - */ - add(object: T): T { - this.objects[object.id] = object; - return object; - } - - /** - * Remove an object from the container - */ - remove(object: T): void { - delete this.objects[object.id]; - } - - /** - * Get an object from the container by its id - */ - get(id: RObjectId): T | null { - return this.objects[id] || null; - } - - /** - * Count the number of objects - */ - count(): number { - return this.list().length; - } - - /** - * Get all contained ids (list) - */ - ids(): RObjectId[] { - return this.list().map(obj => obj.id); - } - - /** - * Get all contained objects (list) - */ - list(): T[] { - return values(this.objects); - } - - /** - * Get all contained objects (iterator) - */ - iterator(): Iterable { - return iarray(this.list()); - } - } +/** + * Returns the id of an object + */ +export function rid(obj: RObject | RObjectId): number { + return (obj instanceof RObject) ? obj.id : obj; +} + +/** + * Referenced objects, with unique ID. + * + * Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer. + */ +export class RObject { + readonly id: RObjectId = RObject._next_id++ + private static _next_id = 0 + + postUnserialize() { + if (this.id >= RObject._next_id) { + RObject._next_id = this.id + 1; + } + } + + /** + * Check that two objects are the same (only by comparing their ID) + */ + is(other: RObject | RObjectId | null): boolean { + if (other === null) { + return false; + } else if (other instanceof RObject) { + return this.id === other.id; + } else { + return this.id === other; + } + } +} + +/** + * Container to track referenced objects + */ +export class RObjectContainer { + private objects: { [index: number]: T } = {} + + constructor(objects: T[] = []) { + objects.forEach(obj => this.add(obj)); + } + + /** + * Add an object to the container + */ + add(object: T): T { + this.objects[object.id] = object; + return object; + } + + /** + * Remove an object from the container + */ + remove(object: T): void { + delete this.objects[object.id]; + } + + /** + * Get an object from the container by its id + */ + get(id: RObjectId): T | null { + return this.objects[id] || null; + } + + /** + * Count the number of objects + */ + count(): number { + return this.list().length; + } + + /** + * Get all contained ids (list) + */ + ids(): RObjectId[] { + return this.list().map(obj => obj.id); + } + + /** + * Get all contained objects (list) + */ + list(): T[] { + return values(this.objects); + } + + /** + * Get all contained objects (iterator) + */ + iterator(): Iterable { + return iarray(this.list()); + } } diff --git a/src/common/RandomGenerator.spec.ts b/src/common/RandomGenerator.spec.ts index c55edf3..e8c3ac3 100644 --- a/src/common/RandomGenerator.spec.ts +++ b/src/common/RandomGenerator.spec.ts @@ -1,92 +1,90 @@ -module TK { - testing("RandomGenerator", test => { - test.case("produces floats", check => { - var gen = new RandomGenerator(); +testing("RandomGenerator", test => { + test.case("produces floats", check => { + var gen = new RandomGenerator(); - var i = 100; - while (i--) { - var value = gen.random(); - check.greaterorequal(value, 0); - check.greater(1, value); - } - }); + var i = 100; + while (i--) { + var value = gen.random(); + check.greaterorequal(value, 0); + check.greater(1, value); + } + }); - test.case("produces integers", check => { - var gen = new RandomGenerator(); + test.case("produces integers", check => { + var gen = new RandomGenerator(); - var i = 100; - while (i--) { - var value = gen.randInt(5, 12); - check.equals(Math.round(value), value); - check.greater(value, 4); - check.greater(13, value); - } - }); + var i = 100; + while (i--) { + var value = gen.randInt(5, 12); + check.equals(Math.round(value), value); + check.greater(value, 4); + check.greater(13, value); + } + }); - test.case("chooses from an array", check => { - var gen = new RandomGenerator(); + test.case("chooses from an array", check => { + var gen = new RandomGenerator(); - check.equals(gen.choice([5]), 5); + check.equals(gen.choice([5]), 5); - var i = 100; - while (i--) { - var value = gen.choice(["test", "thing"]); - check.contains(["thing", "test"], value); - } - }); + var i = 100; + while (i--) { + var value = gen.choice(["test", "thing"]); + check.contains(["thing", "test"], value); + } + }); - test.case("samples from an array", check => { - var gen = new RandomGenerator(); + test.case("samples from an array", check => { + var gen = new RandomGenerator(); - var i = 100; - while (i-- > 1) { - var input = [1, 2, 3, 4, 5]; - var sample = gen.sample(input, i % 5 + 1); - check.same(sample.length, i % 5 + 1); - sample.forEach((num, idx) => { - check.contains(input, num); - check.notcontains(sample.filter((ival, iidx) => iidx != idx), num); - }); - } - }); + var i = 100; + while (i-- > 1) { + var input = [1, 2, 3, 4, 5]; + var sample = gen.sample(input, i % 5 + 1); + check.same(sample.length, i % 5 + 1); + sample.forEach((num, idx) => { + check.contains(input, num); + check.notcontains(sample.filter((ival, iidx) => iidx != idx), num); + }); + } + }); - test.case("choose from weighted ranges", check => { - let gen = new RandomGenerator(); + test.case("choose from weighted ranges", check => { + let gen = new RandomGenerator(); - check.equals(gen.weighted([]), -1); - check.equals(gen.weighted([1]), 0); - check.equals(gen.weighted([0, 1, 0]), 1); - check.equals(gen.weighted([0, 12, 0]), 1); + check.equals(gen.weighted([]), -1); + check.equals(gen.weighted([1]), 0); + check.equals(gen.weighted([0, 1, 0]), 1); + check.equals(gen.weighted([0, 12, 0]), 1); - gen = new SkewedRandomGenerator([0, 0.5, 0.7, 0.8, 0.9999]); - check.equals(gen.weighted([4, 3, 0, 2, 1]), 0); - check.equals(gen.weighted([4, 3, 0, 2, 1]), 1); - check.equals(gen.weighted([4, 3, 0, 2, 1]), 3); - check.equals(gen.weighted([4, 3, 0, 2, 1]), 3); - check.equals(gen.weighted([4, 3, 0, 2, 1]), 4); - }); + gen = new SkewedRandomGenerator([0, 0.5, 0.7, 0.8, 0.9999]); + check.equals(gen.weighted([4, 3, 0, 2, 1]), 0); + check.equals(gen.weighted([4, 3, 0, 2, 1]), 1); + check.equals(gen.weighted([4, 3, 0, 2, 1]), 3); + check.equals(gen.weighted([4, 3, 0, 2, 1]), 3); + check.equals(gen.weighted([4, 3, 0, 2, 1]), 4); + }); - test.case("generates ids", check => { - let gen = new SkewedRandomGenerator([0, 0.4, 0.2, 0.1, 0.3, 0.8]); - check.equals(gen.id(6, "abcdefghij"), "aecbdi"); - }); + test.case("generates ids", check => { + let gen = new SkewedRandomGenerator([0, 0.4, 0.2, 0.1, 0.3, 0.8]); + check.equals(gen.id(6, "abcdefghij"), "aecbdi"); + }); - test.case("can be skewed", check => { - var gen = new SkewedRandomGenerator([0, 0.5, 0.2, 0.9]); + test.case("can be skewed", check => { + var gen = new SkewedRandomGenerator([0, 0.5, 0.2, 0.9]); - check.equals(gen.random(), 0); - check.equals(gen.random(), 0.5); - check.equals(gen.randInt(4, 8), 5); - check.equals(gen.random(), 0.9); + check.equals(gen.random(), 0); + check.equals(gen.random(), 0.5); + check.equals(gen.randInt(4, 8), 5); + check.equals(gen.random(), 0.9); - var value = gen.random(); - check.greaterorequal(value, 0); - check.greater(1, value); + var value = gen.random(); + check.greaterorequal(value, 0); + check.greater(1, value); - gen = new SkewedRandomGenerator([0.7], true); - check.equals(gen.random(), 0.7); - check.equals(gen.random(), 0.7); - check.equals(gen.random(), 0.7); - }); - }); -} + gen = new SkewedRandomGenerator([0.7], true); + check.equals(gen.random(), 0.7); + check.equals(gen.random(), 0.7); + check.equals(gen.random(), 0.7); + }); +}); diff --git a/src/common/RandomGenerator.ts b/src/common/RandomGenerator.ts index e8f8a09..e2b61e8 100644 --- a/src/common/RandomGenerator.ts +++ b/src/common/RandomGenerator.ts @@ -1,114 +1,114 @@ -module TK { - /* - * Random generator. - */ - export class RandomGenerator { - static global: RandomGenerator = new RandomGenerator(); +import { range, sum } from "./Tools"; - postUnserialize() { - this.random = Math.random; - } +/* + * Random generator. + */ +export class RandomGenerator { + static global: RandomGenerator = new RandomGenerator(); - /** - * Get a random number in the (0.0 included -> 1.0 excluded) range - */ - random = Math.random; + postUnserialize() { + this.random = Math.random; + } - /** - * Get a random number in the (*from* included -> *to* included) range - */ - randInt(from: number, to: number): number { - return Math.floor(this.random() * (to - from + 1)) + from; - } + /** + * Get a random number in the (0.0 included -> 1.0 excluded) range + */ + random = Math.random; - /** - * Choose a random item in an array - */ - choice(input: T[]): T { - return input[this.randInt(0, input.length - 1)]; - } + /** + * Get a random number in the (*from* included -> *to* included) range + */ + randInt(from: number, to: number): number { + return Math.floor(this.random() * (to - from + 1)) + from; + } - /** - * Choose a random sample of items from an array - */ - sample(input: T[], count: number): T[] { - var minput = input.slice(); - var result: T[] = []; - while (count--) { - var idx = this.randInt(0, minput.length - 1); - result.push(minput[idx]); - minput.splice(idx, 1); - } - return result; - } + /** + * Choose a random item in an array + */ + choice(input: T[]): T { + return input[this.randInt(0, input.length - 1)]; + } - /** - * Get a random boolean (coin toss) - */ - bool(): boolean { - return this.randInt(0, 1) == 0; - } + /** + * Choose a random sample of items from an array + */ + sample(input: T[], count: number): T[] { + var minput = input.slice(); + var result: T[] = []; + while (count--) { + var idx = this.randInt(0, minput.length - 1); + result.push(minput[idx]); + minput.splice(idx, 1); + } + return result; + } - /** - * Get the range in which the number falls, ranges being weighted - */ - weighted(weights: number[]): number { - if (weights.length == 0) { - return -1; - } + /** + * Get a random boolean (coin toss) + */ + bool(): boolean { + return this.randInt(0, 1) == 0; + } - let total = sum(weights); - if (total == 0) { - return 0; - } else { - let cumul = 0; - weights = weights.map(weight => { - cumul += weight / total; - return cumul; - }); - let r = this.random(); - for (let i = 0; i < weights.length; i++) { - if (r < weights[i]) { - return i; - } - } - return weights.length - 1; - } - } - - /** - * Generate a random id string, composed of ascii characters - */ - id(length: number, chars?: string): string { - if (!chars) { - chars = range(94).map(i => String.fromCharCode(i + 33)).join(""); - } - return range(length).map(() => this.choice(chars)).join(""); - } + /** + * Get the range in which the number falls, ranges being weighted + */ + weighted(weights: number[]): number { + if (weights.length == 0) { + return -1; } - /* - * Random generator that produces a series of fixed numbers before going back to random ones. - */ - export class SkewedRandomGenerator extends RandomGenerator { - i = 0; - suite: number[]; - loop: boolean; - - constructor(suite: number[], loop = false) { - super(); - - this.suite = suite; - this.loop = loop; - } - - random = () => { - var result = this.suite[this.i]; - this.i += 1; - if (this.loop && this.i == this.suite.length) { - this.i = 0; - } - return (typeof result == "undefined") ? Math.random() : result; + let total = sum(weights); + if (total == 0) { + return 0; + } else { + let cumul = 0; + weights = weights.map(weight => { + cumul += weight / total; + return cumul; + }); + let r = this.random(); + for (let i = 0; i < weights.length; i++) { + if (r < weights[i]) { + return i; } + } + return weights.length - 1; } -} \ No newline at end of file + } + + /** + * Generate a random id string, composed of ascii characters + */ + id(length: number, chars?: string): string { + if (!chars) { + chars = range(94).map(i => String.fromCharCode(i + 33)).join(""); + } + return range(length).map(() => this.choice(chars)).join(""); + } +} + +/* + * Random generator that produces a series of fixed numbers before going back to random ones. + */ +export class SkewedRandomGenerator extends RandomGenerator { + i = 0; + suite: number[]; + loop: boolean; + + constructor(suite: number[], loop = false) { + super(); + + this.suite = suite; + this.loop = loop; + } + + random = () => { + var result = this.suite[this.i]; + this.i += 1; + if (this.loop && this.i == this.suite.length) { + this.i = 0; + } + return (typeof result == "undefined") ? Math.random() : result; + } +} diff --git a/src/common/Serializer.spec.ts b/src/common/Serializer.spec.ts index 7ffee31..91d5de2 100644 --- a/src/common/Serializer.spec.ts +++ b/src/common/Serializer.spec.ts @@ -1,104 +1,102 @@ -module TK.Specs { - export class TestSerializerObj1 { - a: number; - constructor(a = 0) { - this.a = a; - } - } - - export class TestSerializerObj2 { - a = () => 1 - b = [(obj: any) => 2] - } - - export class TestSerializerObj3 { - a = [1, 2]; - postUnserialize() { - remove(this.a, 2); - } - } - - testing("Serializer", test => { - function checkReversability(obj: any, namespace = TK.Specs): any { - var serializer = new Serializer(TK.Specs); - var data = serializer.serialize(obj); - serializer = new Serializer(TK.Specs); - var loaded = serializer.unserialize(data); - test.check.equals(loaded, obj); - return loaded; - } - - test.case("serializes simple objects", check => { - var obj = { - "a": 5, - "b": null, - "c": [{ "a": 2 }, "test"] - }; - checkReversability(obj); - }); - - test.case("restores objects constructed from class", check => { - var loaded = checkReversability(new TestSerializerObj1(5)); - check.equals(loaded.a, 5); - check.same(loaded instanceof TestSerializerObj1, true, "not a TestSerializerObj1 instance"); - }); - - test.case("stores one version of the same object", check => { - var a = new TestSerializerObj1(8); - var b = new TestSerializerObj1(8); - var c = { - 'r': a, - 's': ["test", a], - 't': a, - 'u': b - }; - var loaded = checkReversability(c); - check.same(loaded.t, loaded.r); - check.same(loaded.s[1], loaded.r); - check.notsame(loaded.u, loaded.r); - }); - - test.case("handles circular references", check => { - var a: any = { b: {} }; - a.b.c = a; - - var loaded = checkReversability(a); - }); - - test.case("ignores some classes", check => { - var serializer = new Serializer(TK.Specs); - serializer.addIgnoredClass("TestSerializerObj1"); - - var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); - var loaded = serializer.unserialize(data); - - check.equals(loaded, { a: 5, b: undefined }); - }); - - test.case("ignores functions", check => { - let serializer = new Serializer(TK.Specs); - let data = serializer.serialize({ obj: new TestSerializerObj2() }); - let loaded = serializer.unserialize(data); - - let expected = new TestSerializerObj2(); - expected.a = undefined; - expected.b[0] = undefined; - check.equals(loaded, { obj: expected }); - }); - - test.case("calls specific postUnserialize", check => { - let serializer = new Serializer(TK.Specs); - let data = serializer.serialize({ obj: new TestSerializerObj3() }); - let loaded = serializer.unserialize(data); - - let expected = new TestSerializerObj3(); - expected.a = [1]; - check.equals(loaded, { obj: expected }); - }); - - test.case("finds TS namespace, even from sub-namespace", check => { - checkReversability(new Timer()); - checkReversability(new RandomGenerator()); - }); - }); +export class TestSerializerObj1 { + a: number; + constructor(a = 0) { + this.a = a; + } } + +export class TestSerializerObj2 { + a = () => 1 + b = [(obj: any) => 2] +} + +export class TestSerializerObj3 { + a = [1, 2]; + postUnserialize() { + remove(this.a, 2); + } +} + +testing("Serializer", test => { + function checkReversability(obj: any, namespace = TK.Specs): any { + var serializer = new Serializer(TK.Specs); + var data = serializer.serialize(obj); + serializer = new Serializer(TK.Specs); + var loaded = serializer.unserialize(data); + test.check.equals(loaded, obj); + return loaded; + } + + test.case("serializes simple objects", check => { + var obj = { + "a": 5, + "b": null, + "c": [{ "a": 2 }, "test"] + }; + checkReversability(obj); + }); + + test.case("restores objects constructed from class", check => { + var loaded = checkReversability(new TestSerializerObj1(5)); + check.equals(loaded.a, 5); + check.same(loaded instanceof TestSerializerObj1, true, "not a TestSerializerObj1 instance"); + }); + + test.case("stores one version of the same object", check => { + var a = new TestSerializerObj1(8); + var b = new TestSerializerObj1(8); + var c = { + 'r': a, + 's': ["test", a], + 't': a, + 'u': b + }; + var loaded = checkReversability(c); + check.same(loaded.t, loaded.r); + check.same(loaded.s[1], loaded.r); + check.notsame(loaded.u, loaded.r); + }); + + test.case("handles circular references", check => { + var a: any = { b: {} }; + a.b.c = a; + + var loaded = checkReversability(a); + }); + + test.case("ignores some classes", check => { + var serializer = new Serializer(TK.Specs); + serializer.addIgnoredClass("TestSerializerObj1"); + + var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); + var loaded = serializer.unserialize(data); + + check.equals(loaded, { a: 5, b: undefined }); + }); + + test.case("ignores functions", check => { + let serializer = new Serializer(TK.Specs); + let data = serializer.serialize({ obj: new TestSerializerObj2() }); + let loaded = serializer.unserialize(data); + + let expected = new TestSerializerObj2(); + expected.a = undefined; + expected.b[0] = undefined; + check.equals(loaded, { obj: expected }); + }); + + test.case("calls specific postUnserialize", check => { + let serializer = new Serializer(TK.Specs); + let data = serializer.serialize({ obj: new TestSerializerObj3() }); + let loaded = serializer.unserialize(data); + + let expected = new TestSerializerObj3(); + expected.a = [1]; + check.equals(loaded, { obj: expected }); + }); + + test.case("finds TS namespace, even from sub-namespace", check => { + checkReversability(new Timer()); + checkReversability(new RandomGenerator()); + }); +}); diff --git a/src/common/Serializer.ts b/src/common/Serializer.ts index 669f446..5a5e294 100644 --- a/src/common/Serializer.ts +++ b/src/common/Serializer.ts @@ -1,111 +1,105 @@ -module TK { +import { add, classname, crawl, merge, STOP_CRAWLING } from "./Tools"; - function isObject(value: any): boolean { - return value instanceof Object && !Array.isArray(value); - } - - /** - * A typescript object serializer. - */ - export class Serializer { - namespace: any; - ignored: string[] = []; - - constructor(namespace: any = TK) { - this.namespace = namespace; - } - - /** - * Add a class to the ignore list - */ - addIgnoredClass(name: string) { - this.ignored.push(name); - } - - /** - * Construct an object from a constructor name - */ - constructObject(ctype: string): Object { - if (ctype == "Object") { - return {}; - } else { - let cl = this.namespace[ctype]; - if (cl) { - return Object.create(cl.prototype); - } else { - cl = (TK)[ctype]; - if (cl) { - return Object.create(cl.prototype); - } else { - console.error("Can't find class", ctype); - return {}; - } - } - } - } - - /** - * Serialize an object to a string - */ - serialize(obj: any): string { - // Collect objects - var objects: Object[] = []; - var stats: any = {}; - crawl(obj, value => { - if (isObject(value)) { - var vtype = classname(value); - if (vtype != "" && this.ignored.indexOf(vtype) < 0) { - stats[vtype] = (stats[vtype] || 0) + 1; - add(objects, value); - return value; - } else { - return TK.STOP_CRAWLING; - } - } else { - return value; - } - }); - //console.log("Serialize stats", stats); - - // Serialize objects list, transforming deeper objects to links - var fobjects = objects.map(value => { $c: classname(value), $f: merge({}, value) }); - return JSON.stringify(fobjects, (key, value) => { - if (key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i")) { - return { $i: objects.indexOf(value) }; - } else { - return value; - } - }); - } - - /** - * Unserialize a string to an object - */ - unserialize(data: string): any { - // Unserialize objects list - var fobjects = JSON.parse(data); - - // Reconstruct objects - var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f'])); - - // Reconnect links - crawl(objects, value => { - if (value instanceof Object && value.hasOwnProperty('$i')) { - return objects[value['$i']]; - } else { - return value; - } - }, true); - - // Post unserialize hooks - crawl(objects[0], value => { - if (value instanceof Object && typeof value.postUnserialize == "function") { - value.postUnserialize(); - } - }); - - // First object was the root - return objects[0]; - } - } +function isObject(value: any): boolean { + return value instanceof Object && !Array.isArray(value); +} + +/** + * A typescript object serializer. + */ +export class Serializer { + namespace: { [name: string]: (...args: any) => any }; + ignored: string[] = []; + + constructor(namespace: { [name: string]: (...args: any) => any } = {}) { + this.namespace = namespace; + } + + /** + * Add a class to the ignore list + */ + addIgnoredClass(name: string) { + this.ignored.push(name); + } + + /** + * Construct an object from a constructor name + */ + constructObject(ctype: string): Object { + if (ctype == "Object") { + return {}; + } else { + let cl = this.namespace[ctype]; + if (cl) { + return Object.create(cl.prototype); + } else { + console.error("Can't find class", ctype); + return {}; + } + } + } + + /** + * Serialize an object to a string + */ + serialize(obj: any): string { + // Collect objects + var objects: Object[] = []; + var stats: any = {}; + crawl(obj, value => { + if (isObject(value)) { + var vtype = classname(value); + if (vtype != "" && this.ignored.indexOf(vtype) < 0) { + stats[vtype] = (stats[vtype] || 0) + 1; + add(objects, value); + return value; + } else { + return STOP_CRAWLING; + } + } else { + return value; + } + }); + //console.log("Serialize stats", stats); + + // Serialize objects list, transforming deeper objects to links + var fobjects = objects.map(value => { $c: classname(value), $f: merge({}, value) }); + return JSON.stringify(fobjects, (key, value) => { + if (key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i")) { + return { $i: objects.indexOf(value) }; + } else { + return value; + } + }); + } + + /** + * Unserialize a string to an object + */ + unserialize(data: string): any { + // Unserialize objects list + var fobjects = JSON.parse(data); + + // Reconstruct objects + var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f'])); + + // Reconnect links + crawl(objects, value => { + if (value instanceof Object && value.hasOwnProperty('$i')) { + return objects[value['$i']]; + } else { + return value; + } + }, true); + + // Post unserialize hooks + crawl(objects[0], value => { + if (value instanceof Object && typeof value.postUnserialize == "function") { + value.postUnserialize(); + } + }); + + // First object was the root + return objects[0]; + } } diff --git a/src/common/Testing.ts b/src/common/Testing.ts index 6a123f5..d67957d 100644 --- a/src/common/Testing.ts +++ b/src/common/Testing.ts @@ -1,330 +1,327 @@ +import { Timer } from "./Timer"; + +export type FakeClock = { forward: (milliseconds: number) => void } +export type Mock = { func: F, getCalls: () => any[][], reset: () => void } + /** - * Various testing functions. + * Main test suite descriptor */ -module TK { - export type FakeClock = { forward: (milliseconds: number) => void } - export type Mock = { func: F, getCalls: () => any[][], reset: () => void } +export function testing(desc: string, body: (test: TestSuite) => void) { + if (typeof describe != "undefined") { + describe(desc, () => { + beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS)); - /** - * Main test suite descriptor - */ - export function testing(desc: string, body: (test: TestSuite) => void) { - if (typeof describe != "undefined") { - describe(desc, () => { - beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS)); + let test = new TestSuite(desc); + body(test); + }); + } +} - let test = new TestSuite(desc); - body(test); - }); - } +/** + * Test suite (group of test cases) + */ +export class TestSuite { + private desc: string + constructor(desc: string) { + this.desc = desc; + } + + /** + * Add a setup step for each case of the suite + */ + setup(body: Function, cleanup?: Function): void { + beforeEach(() => body()); + if (cleanup) { + afterEach(() => cleanup()); + } + } + + /** + * Add an asynchronous setup step for each case of the suite + */ + asetup(body: () => Promise, cleanup?: () => Promise): void { + beforeEach(async () => await body()); + if (cleanup) { + afterEach(async () => await cleanup()); + } + } + + /** + * Describe a single test case + */ + case(desc: string, body: (ctx: TestContext) => void): void { + it(desc, () => { + console.debug(`${this.desc} ${desc}`); + body(new TestContext()) + }); + } + + /** + * Describe an asynchronous test case + */ + acase(desc: string, body: (ctx: TestContext) => Promise): void { + it(desc, done => { + console.debug(`${this.desc} ${desc}`); + body(new TestContext()).then(done).catch(done.fail); + }); + } + + /** + * Setup fake clock for the suite + */ + clock(): FakeClock { + let current = 0; + + beforeEach(function () { + current = 0; + jasmine.clock().install(); + spyOn(Timer, "nowMs").and.callFake(() => current); + }); + + afterEach(function () { + jasmine.clock().uninstall(); + }); + + return { + forward: milliseconds => { + current += milliseconds; + jasmine.clock().tick(milliseconds); + } + }; + } + + /** + * Out-of-context assertion helpers + * + * It is better to use in-context checks, for better information + */ + get check(): TestContext { + return new TestContext(); + } +} + +/** + * A test context, with assertion helpers + */ +export class TestContext { + info: string[]; + + constructor(info: string[] = []) { + this.info = info; + } + + /** + * Create a sub context (adds information for all assertions done with this context) + */ + sub(info: string): TestContext { + return new TestContext(this.info.concat([info])); + } + + /** + * Execute a body in a sub context + */ + in(info: string, body: (ctx: TestContext) => void): void { + body(this.sub(info)); + } + + /** + * Builds a message, with context information added + */ + message(message?: string): string | undefined { + let parts = this.info; + if (message) { + parts = parts.concat([message]); + } + return parts.length ? parts.join(" - ") : undefined; + } + + /** + * Patch an object's method with a mock + * + * Replacement may be: + * - undefined to call through + * - null to not call anything + * - a fake function to call instead + * + * All patches are removed at the end of a case + */ + patch(obj: T, method: K, replacement?: F | null): Mock { + let spy = spyOn(obj, method); + if (replacement === null) { + spy.and.stub(); + } else if (replacement) { + spy.and.callFake(replacement); + } else { + spy.and.callThrough(); } - /** - * Test suite (group of test cases) - */ - export class TestSuite { - private desc: string - constructor(desc: string) { - this.desc = desc; - } + return { + func: spy, + getCalls: () => spy.calls.all().map(info => info.args), + reset: () => spy.calls.reset() + } + } - /** - * Add a setup step for each case of the suite - */ - setup(body: Function, cleanup?: Function): void { - beforeEach(() => body()); - if (cleanup) { - afterEach(() => cleanup()); - } - } + /** + * Create a mock function + */ + mockfunc(name = "mock", replacement?: F): Mock { + let spy = jasmine.createSpy(name, replacement); - /** - * Add an asynchronous setup step for each case of the suite - */ - asetup(body: () => Promise, cleanup?: () => Promise): void { - beforeEach(async () => await body()); - if (cleanup) { - afterEach(async () => await cleanup()); - } - } - - /** - * Describe a single test case - */ - case(desc: string, body: (ctx: TestContext) => void): void { - it(desc, () => { - console.debug(`${this.desc} ${desc}`); - body(new TestContext()) - }); - } - - /** - * Describe an asynchronous test case - */ - acase(desc: string, body: (ctx: TestContext) => Promise): void { - it(desc, done => { - console.debug(`${this.desc} ${desc}`); - body(new TestContext()).then(done).catch(done.fail); - }); - } - - /** - * Setup fake clock for the suite - */ - clock(): FakeClock { - let current = 0; - - beforeEach(function () { - current = 0; - jasmine.clock().install(); - spyOn(Timer, "nowMs").and.callFake(() => current); - }); - - afterEach(function () { - jasmine.clock().uninstall(); - }); - - return { - forward: milliseconds => { - current += milliseconds; - jasmine.clock().tick(milliseconds); - } - }; - } - - /** - * Out-of-context assertion helpers - * - * It is better to use in-context checks, for better information - */ - get check(): TestContext { - return new TestContext(); - } + if (replacement) { + spy = spy.and.callThrough(); } - /** - * A test context, with assertion helpers - */ - export class TestContext { - info: string[]; + return { + func: spy, + getCalls: () => spy.calls.all().map(info => info.args), + reset: () => spy.calls.reset() + } + } - constructor(info: string[] = []) { - this.info = info; - } - - /** - * Create a sub context (adds information for all assertions done with this context) - */ - sub(info: string): TestContext { - return new TestContext(this.info.concat([info])); - } - - /** - * Execute a body in a sub context - */ - in(info: string, body: (ctx: TestContext) => void): void { - body(this.sub(info)); - } - - /** - * Builds a message, with context information added - */ - message(message?: string): string | undefined { - let parts = this.info; - if (message) { - parts = parts.concat([message]); - } - return parts.length ? parts.join(" - ") : undefined; - } - - /** - * Patch an object's method with a mock - * - * Replacement may be: - * - undefined to call through - * - null to not call anything - * - a fake function to call instead - * - * All patches are removed at the end of a case - */ - patch(obj: T, method: K, replacement?: F | null): Mock { - let spy = spyOn(obj, method); - if (replacement === null) { - spy.and.stub(); - } else if (replacement) { - spy.and.callFake(replacement); - } else { - spy.and.callThrough(); - } - - return { - func: spy, - getCalls: () => spy.calls.all().map(info => info.args), - reset: () => spy.calls.reset() - } - } - - /** - * Create a mock function - */ - mockfunc(name = "mock", replacement?: F): Mock { - let spy = jasmine.createSpy(name, replacement); - - if (replacement) { - spy = spy.and.callThrough(); - } - - return { - func: spy, - getCalls: () => spy.calls.all().map(info => info.args), - reset: () => spy.calls.reset() - } - } - - /** - * Check that a mock have been called a given number of times, or with specific args - */ - called(mock: Mock, calls: number | any[][], reset = true): void { - if (typeof calls == "number") { - expect(mock.getCalls().length).toEqual(calls, this.message()); - } else { - expect(mock.getCalls()).toEqual(calls, this.message()); - } - - if (reset) { - mock.reset(); - } - } - - /** - * Check that a function call throws an error - */ - throw(call: Function, error?: string | Error): void { - if (typeof error == "undefined") { - expect(call).toThrow(); - } else if (typeof error == "string") { - expect(call).toThrowError(error); - } else { - expect(call).toThrow(error); - } - } - - /** - * Check that an object is an instance of a given type - */ - instance(obj: any, classref: { new(...args: any[]): T }, message: string): obj is T { - let result = obj instanceof classref; - expect(result).toBe(true, this.message(message)); - return result; - } - - /** - * Check that two references are the same object - */ - same(ref1: T | null | undefined, ref2: T | null | undefined, message?: string): void { - expect(ref1).toBe(ref2, this.message(message)); - } - - /** - * Check that two references are not the same object - */ - notsame(ref1: T | null, ref2: T | null, message?: string): void { - expect(ref1).not.toBe(ref2, this.message(message)); - } - - /** - * Check that two values are equal, in the sense of deep comparison - */ - equals(val1: T, val2: T, message?: string): void { - expect(val1).toEqual(val2, this.message(message)); - } - - /** - * Check that two values differs, in the sense of deep comparison - */ - notequals(val1: T, val2: T, message?: string): void { - expect(val1).not.toEqual(val2, this.message(message)); - } - - /** - * Check that a numerical value is close to another, at a given number of digits precision - */ - nears(val1: number, val2: number, precision = 8, message?: string): void { - if (precision != Math.round(precision)) { - throw new Error(`'nears' precision should be integer, not {precision}`); - } - expect(val1).toBeCloseTo(val2, precision, this.message(message)); - } - - /** - * Check that a numerical value is greater than another - */ - greater(val1: number, val2: number, message?: string): void { - expect(val1).toBeGreaterThan(val2, this.message(message)); - } - - /** - * Check that a numerical value is greater than or equal to another - */ - greaterorequal(val1: number, val2: number, message?: string): void { - expect(val1).toBeGreaterThanOrEqual(val2, this.message(message)); - } - - /** - * Check that a string matches a regex - */ - regex(pattern: RegExp, value: string, message?: string): void { - expect(value).toMatch(pattern, this.message(message)); - } - - /** - * Check that an array contains an item - */ - contains(array: T[], item: T, message?: string): void { - expect(array).toContain(item, this.message(message)); - } - - /** - * Check that an array does not contain an item - */ - notcontains(array: T[], item: T, message?: string): void { - expect(array).not.toContain(item, this.message(message)); - } - - /** - * Check than an object contains a set of properties - */ - containing(val: T, props: Partial, message?: string): void { - expect(val).toEqual(jasmine.objectContaining(props), this.message(message)); - } - - /** - * Fail the whole case - */ - fail(message?: string): void { - fail(this.message(message)); - } + /** + * Check that a mock have been called a given number of times, or with specific args + */ + called(mock: Mock, calls: number | any[][], reset = true): void { + if (typeof calls == "number") { + expect(mock.getCalls().length).toEqual(calls, this.message()); + } else { + expect(mock.getCalls()).toEqual(calls, this.message()); } - const CUSTOM_MATCHERS = { - toEqual: function (util: any, customEqualityTesters: any) { - customEqualityTesters = customEqualityTesters || []; - - return { - compare: function (actual: any, expected: any, message?: string) { - let result: any = { pass: false }; - let diffBuilder = (jasmine).DiffBuilder(); - - result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder); - - result.message = diffBuilder.getMessage(); - if (message) { - result.message += " " + message; - } - - return result; - } - }; - } + if (reset) { + mock.reset(); } -} \ No newline at end of file + } + + /** + * Check that a function call throws an error + */ + throw(call: Function, error?: string | Error): void { + if (typeof error == "undefined") { + expect(call).toThrow(); + } else if (typeof error == "string") { + expect(call).toThrowError(error); + } else { + expect(call).toThrow(error); + } + } + + /** + * Check that an object is an instance of a given type + */ + instance(obj: any, classref: { new(...args: any[]): T }, message: string): obj is T { + let result = obj instanceof classref; + expect(result).toBe(true, this.message(message)); + return result; + } + + /** + * Check that two references are the same object + */ + same(ref1: T | null | undefined, ref2: T | null | undefined, message?: string): void { + expect(ref1).toBe(ref2, this.message(message)); + } + + /** + * Check that two references are not the same object + */ + notsame(ref1: T | null, ref2: T | null, message?: string): void { + expect(ref1).not.toBe(ref2, this.message(message)); + } + + /** + * Check that two values are equal, in the sense of deep comparison + */ + equals(val1: T, val2: T, message?: string): void { + expect(val1).toEqual(val2, this.message(message)); + } + + /** + * Check that two values differs, in the sense of deep comparison + */ + notequals(val1: T, val2: T, message?: string): void { + expect(val1).not.toEqual(val2, this.message(message)); + } + + /** + * Check that a numerical value is close to another, at a given number of digits precision + */ + nears(val1: number, val2: number, precision = 8, message?: string): void { + if (precision != Math.round(precision)) { + throw new Error(`'nears' precision should be integer, not {precision}`); + } + expect(val1).toBeCloseTo(val2, precision, this.message(message)); + } + + /** + * Check that a numerical value is greater than another + */ + greater(val1: number, val2: number, message?: string): void { + expect(val1).toBeGreaterThan(val2, this.message(message)); + } + + /** + * Check that a numerical value is greater than or equal to another + */ + greaterorequal(val1: number, val2: number, message?: string): void { + expect(val1).toBeGreaterThanOrEqual(val2, this.message(message)); + } + + /** + * Check that a string matches a regex + */ + regex(pattern: RegExp, value: string, message?: string): void { + expect(value).toMatch(pattern, this.message(message)); + } + + /** + * Check that an array contains an item + */ + contains(array: T[], item: T, message?: string): void { + expect(array).toContain(item, this.message(message)); + } + + /** + * Check that an array does not contain an item + */ + notcontains(array: T[], item: T, message?: string): void { + expect(array).not.toContain(item, this.message(message)); + } + + /** + * Check than an object contains a set of properties + */ + containing(val: T, props: Partial, message?: string): void { + expect(val).toEqual(jasmine.objectContaining(props), this.message(message)); + } + + /** + * Fail the whole case + */ + fail(message?: string): void { + fail(this.message(message)); + } +} + +const CUSTOM_MATCHERS = { + toEqual: function (util: any, customEqualityTesters: any) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function (actual: any, expected: any, message?: string) { + let result: any = { pass: false }; + let diffBuilder = (jasmine).DiffBuilder(); + + result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder); + + result.message = diffBuilder.getMessage(); + if (message) { + result.message += " " + message; + } + + return result; + } + }; + } +} diff --git a/src/common/Timer.spec.ts b/src/common/Timer.spec.ts index 1896466..7d5873c 100644 --- a/src/common/Timer.spec.ts +++ b/src/common/Timer.spec.ts @@ -1,117 +1,115 @@ -module TK.Specs { - testing("Timer", test => { - let clock = test.clock(); +testing("Timer", test => { + let clock = test.clock(); - test.case("schedules and cancels future calls", check => { - let timer = new Timer(); + test.case("schedules and cancels future calls", check => { + let timer = new Timer(); - let called: any[] = []; - let callback = (item: any) => called.push(item); + let called: any[] = []; + let callback = (item: any) => called.push(item); - let s1 = timer.schedule(50, () => callback(1)); - let s2 = timer.schedule(150, () => callback(2)); - let s3 = timer.schedule(250, () => callback(3)); + let s1 = timer.schedule(50, () => callback(1)); + let s2 = timer.schedule(150, () => callback(2)); + let s3 = timer.schedule(250, () => callback(3)); - check.equals(called, []); - clock.forward(100); - check.equals(called, [1]); - timer.cancel(s1); - check.equals(called, [1]); - clock.forward(100); - check.equals(called, [1, 2]); - timer.cancel(s3); - clock.forward(100); - check.equals(called, [1, 2]); - clock.forward(1000); - check.equals(called, [1, 2]); - }); + check.equals(called, []); + clock.forward(100); + check.equals(called, [1]); + timer.cancel(s1); + check.equals(called, [1]); + clock.forward(100); + check.equals(called, [1, 2]); + timer.cancel(s3); + clock.forward(100); + check.equals(called, [1, 2]); + clock.forward(1000); + check.equals(called, [1, 2]); + }); - test.case("may cancel all scheduled", check => { - let timer = new Timer(); + test.case("may cancel all scheduled", check => { + let timer = new Timer(); - let called: any[] = []; - let callback = (item: any) => called.push(item); + let called: any[] = []; + let callback = (item: any) => called.push(item); - timer.schedule(150, () => callback(1)); - timer.schedule(50, () => callback(2)); - timer.schedule(500, () => callback(3)); + timer.schedule(150, () => callback(1)); + timer.schedule(50, () => callback(2)); + timer.schedule(500, () => callback(3)); - check.equals(called, []); + check.equals(called, []); - clock.forward(100); + clock.forward(100); - check.equals(called, [2]); + check.equals(called, [2]); - clock.forward(100); + clock.forward(100); - check.equals(called, [2, 1]); + check.equals(called, [2, 1]); - timer.cancelAll(); + timer.cancelAll(); - clock.forward(1000); + clock.forward(1000); - check.equals(called, [2, 1]); + check.equals(called, [2, 1]); - timer.schedule(50, () => callback(4)); - timer.schedule(150, () => callback(5)); + timer.schedule(50, () => callback(4)); + timer.schedule(150, () => callback(5)); - clock.forward(100); + clock.forward(100); - check.equals(called, [2, 1, 4]); + check.equals(called, [2, 1, 4]); - timer.cancelAll(true); + timer.cancelAll(true); - clock.forward(100); + clock.forward(100); - check.equals(called, [2, 1, 4]); + check.equals(called, [2, 1, 4]); - timer.schedule(50, () => callback(6)); + timer.schedule(50, () => callback(6)); - clock.forward(100); + clock.forward(100); - check.equals(called, [2, 1, 4]); - }); + check.equals(called, [2, 1, 4]); + }); - test.case("may switch to synchronous mode", check => { - let timer = new Timer(true); - let called: any[] = []; - let callback = (item: any) => called.push(item); + test.case("may switch to synchronous mode", check => { + let timer = new Timer(true); + let called: any[] = []; + let callback = (item: any) => called.push(item); - timer.schedule(50, () => callback(1)); - check.equals(called, [1]); - }); + timer.schedule(50, () => callback(1)); + check.equals(called, [1]); + }); - test.acase("sleeps asynchronously", async check => { - let timer = new Timer(); - let x = 1; + test.acase("sleeps asynchronously", async check => { + let timer = new Timer(); + let x = 1; - let promise = timer.sleep(500).then(() => { - x++; - }); - check.equals(x, 1); - - clock.forward(300); - check.equals(x, 1); - - clock.forward(300); - check.equals(x, 1); - - await promise; - check.equals(x, 2); - }); - - test.case("gives current time in milliseconds", check => { - check.equals(Timer.nowMs(), 0); - - clock.forward(5); - - check.equals(Timer.nowMs(), 5); - let t = Timer.nowMs(); - - clock.forward(10); - - check.equals(Timer.nowMs(), 15); - check.equals(Timer.fromMs(t), 10); - }); + let promise = timer.sleep(500).then(() => { + x++; }); -} \ No newline at end of file + check.equals(x, 1); + + clock.forward(300); + check.equals(x, 1); + + clock.forward(300); + check.equals(x, 1); + + await promise; + check.equals(x, 2); + }); + + test.case("gives current time in milliseconds", check => { + check.equals(Timer.nowMs(), 0); + + clock.forward(5); + + check.equals(Timer.nowMs(), 5); + let t = Timer.nowMs(); + + clock.forward(10); + + check.equals(Timer.nowMs(), 15); + check.equals(Timer.fromMs(t), 10); + }); +}); diff --git a/src/common/Timer.ts b/src/common/Timer.ts index 93eaa23..13366ee 100644 --- a/src/common/Timer.ts +++ b/src/common/Timer.ts @@ -1,100 +1,100 @@ -module TK { - /** - * Timing utility. - * - * This extends the standard setTimeout feature. - */ - export class Timer { - // Global timer shared by the whole project - static global = new Timer(); +import { add, remove } from "./Tools"; - // Global synchronous timer for unit tests - static synchronous = new Timer(true); +/** + * Timing utility. + * + * This extends the standard setTimeout feature. + */ +export class Timer { + // Global timer shared by the whole project + static global = new Timer(); - private sync = false; + // Global synchronous timer for unit tests + static synchronous = new Timer(true); - private locked = false; + private sync = false; - private scheduled: any[] = []; + private locked = false; - constructor(sync = false) { - this.sync = sync; - } + private scheduled: any[] = []; - /** - * Get the current timestamp in milliseconds - */ - static nowMs(): number { - return (new Date()).getTime(); - } + constructor(sync = false) { + this.sync = sync; + } - /** - * Get the elapsed time in milliseconds since a previous timestamp - */ - static fromMs(previous: number): number { - return this.nowMs() - previous; - } + /** + * Get the current timestamp in milliseconds + */ + static nowMs(): number { + return (new Date()).getTime(); + } - /** - * Return true if the timer is synchronous - */ - isSynchronous() { - return this.sync; - } + /** + * Get the elapsed time in milliseconds since a previous timestamp + */ + static fromMs(previous: number): number { + return this.nowMs() - previous; + } - /** - * Cancel all scheduled calls. - * - * If lock=true, no further scheduling will be allowed. - */ - cancelAll(lock = false) { - this.locked = lock; + /** + * Return true if the timer is synchronous + */ + isSynchronous() { + return this.sync; + } - let scheduled = this.scheduled; - this.scheduled = []; + /** + * Cancel all scheduled calls. + * + * If lock=true, no further scheduling will be allowed. + */ + cancelAll(lock = false) { + this.locked = lock; - scheduled.forEach(handle => clearTimeout(handle)); - } + let scheduled = this.scheduled; + this.scheduled = []; - /** - * Cancel a scheduled callback. - */ - cancel(scheduled: any) { - if (remove(this.scheduled, scheduled)) { - clearTimeout(scheduled); - } - } + scheduled.forEach(handle => clearTimeout(handle)); + } - /** - * Schedule a callback to be called later (time is in milliseconds). - */ - schedule(delay: number, callback: Function): any { - if (this.locked) { - return null; - } else if (this.sync || delay <= 0) { - callback(); - return null; - } else { - let handle = setTimeout(() => { - remove(this.scheduled, handle); - callback(); - }, delay); - add(this.scheduled, handle); - return handle; - } - } - - /** - * Asynchronously sleep a given time. - */ - async sleep(ms: number): Promise { - return new Promise(resolve => { - this.schedule(ms, resolve); - }); - } - - postUnserialize() { - this.scheduled = []; - } + /** + * Cancel a scheduled callback. + */ + cancel(scheduled: any) { + if (remove(this.scheduled, scheduled)) { + clearTimeout(scheduled); } + } + + /** + * Schedule a callback to be called later (time is in milliseconds). + */ + schedule(delay: number, callback: Function): any { + if (this.locked) { + return null; + } else if (this.sync || delay <= 0) { + callback(); + return null; + } else { + let handle = setTimeout(() => { + remove(this.scheduled, handle); + callback(); + }, delay); + add(this.scheduled, handle); + return handle; + } + } + + /** + * Asynchronously sleep a given time. + */ + async sleep(ms: number): Promise { + return new Promise(resolve => { + this.schedule(ms, resolve); + }); + } + + postUnserialize() { + this.scheduled = []; + } } diff --git a/src/common/Toggle.spec.ts b/src/common/Toggle.spec.ts index 08b57ae..254cbf0 100644 --- a/src/common/Toggle.spec.ts +++ b/src/common/Toggle.spec.ts @@ -1,158 +1,156 @@ -module TK.Specs { - testing("Toggle", test => { - let on_calls = 0; - let off_calls = 0; - let clock = test.clock(); +testing("Toggle", test => { + let on_calls = 0; + let off_calls = 0; + let clock = test.clock(); - test.setup(function () { - on_calls = 0; - off_calls = 0; - }); + test.setup(function () { + on_calls = 0; + off_calls = 0; + }); - function newToggle(): Toggle { - return new Toggle(() => on_calls++, () => off_calls++); - } + function newToggle(): Toggle { + return new Toggle(() => on_calls++, () => off_calls++); + } - function checkstate(on: number, off: number) { - test.check.same(on_calls, on); - test.check.same(off_calls, off); - on_calls = 0; - off_calls = 0; - } + function checkstate(on: number, off: number) { + test.check.same(on_calls, on); + test.check.same(off_calls, off); + on_calls = 0; + off_calls = 0; + } - test.case("toggles on and off", check => { - let toggle = newToggle(); - let client = toggle.manipulate("test"); - checkstate(0, 0); + test.case("toggles on and off", check => { + let toggle = newToggle(); + let client = toggle.manipulate("test"); + checkstate(0, 0); - let result = client(true); - check.equals(result, true); - checkstate(1, 0); + let result = client(true); + check.equals(result, true); + checkstate(1, 0); - result = client(true); - check.equals(result, true); - checkstate(0, 0); + result = client(true); + check.equals(result, true); + checkstate(0, 0); - clock.forward(10000000); - checkstate(0, 0); + clock.forward(10000000); + checkstate(0, 0); - result = client(false); - check.equals(result, false); - checkstate(0, 1); + result = client(false); + check.equals(result, false); + checkstate(0, 1); - result = client(false); - check.equals(result, false); - checkstate(0, 0); + result = client(false); + check.equals(result, false); + checkstate(0, 0); - clock.forward(10000000); - checkstate(0, 0); + clock.forward(10000000); + checkstate(0, 0); - result = client(true); - check.equals(result, true); - checkstate(1, 0); + result = client(true); + check.equals(result, true); + checkstate(1, 0); - let client2 = toggle.manipulate("test2"); - result = client2(true); - check.equals(result, true); - checkstate(0, 0); + let client2 = toggle.manipulate("test2"); + result = client2(true); + check.equals(result, true); + checkstate(0, 0); - result = client(false); - check.equals(result, true); - checkstate(0, 0); + result = client(false); + check.equals(result, true); + checkstate(0, 0); - result = client2(false); - check.equals(result, false); - checkstate(0, 1); - }) + result = client2(false); + check.equals(result, false); + checkstate(0, 1); + }) - test.case("switches between on and off", check => { - let toggle = newToggle(); - let client = toggle.manipulate("test"); - checkstate(0, 0); + test.case("switches between on and off", check => { + let toggle = newToggle(); + let client = toggle.manipulate("test"); + checkstate(0, 0); - let result = client(); - check.equals(result, true); - checkstate(1, 0); + let result = client(); + check.equals(result, true); + checkstate(1, 0); - result = client(); - check.equals(result, false); - checkstate(0, 1); + result = client(); + check.equals(result, false); + checkstate(0, 1); - result = client(); - check.equals(result, true); - checkstate(1, 0); + result = client(); + check.equals(result, true); + checkstate(1, 0); - let client2 = toggle.manipulate("test2"); - checkstate(0, 0); + let client2 = toggle.manipulate("test2"); + checkstate(0, 0); - result = client2(); - check.equals(result, true); - checkstate(0, 0); + result = client2(); + check.equals(result, true); + checkstate(0, 0); - result = client(); - check.equals(result, true); - checkstate(0, 0); + result = client(); + check.equals(result, true); + checkstate(0, 0); - result = client2(); - check.equals(result, false); - checkstate(0, 1); - }) + result = client2(); + check.equals(result, false); + checkstate(0, 1); + }) - test.case("toggles on for a given time", check => { - let toggle = newToggle(); - let client = toggle.manipulate("test"); - checkstate(0, 0); + test.case("toggles on for a given time", check => { + let toggle = newToggle(); + let client = toggle.manipulate("test"); + checkstate(0, 0); - let result = client(100); - check.equals(result, true); - checkstate(1, 0); + let result = client(100); + check.equals(result, true); + checkstate(1, 0); - check.equals(toggle.isOn(), true); - checkstate(0, 0); - clock.forward(60); - check.equals(toggle.isOn(), true); - checkstate(0, 0); - clock.forward(60); - check.equals(toggle.isOn(), false); - checkstate(0, 1); + check.equals(toggle.isOn(), true); + checkstate(0, 0); + clock.forward(60); + check.equals(toggle.isOn(), true); + checkstate(0, 0); + clock.forward(60); + check.equals(toggle.isOn(), false); + checkstate(0, 1); - result = client(100); - check.equals(result, true); - checkstate(1, 0); - result = client(200); - check.equals(result, true); - checkstate(0, 0); - clock.forward(150); - check.equals(toggle.isOn(), true); - checkstate(0, 0); - clock.forward(150); - check.equals(toggle.isOn(), false); - checkstate(0, 1); + result = client(100); + check.equals(result, true); + checkstate(1, 0); + result = client(200); + check.equals(result, true); + checkstate(0, 0); + clock.forward(150); + check.equals(toggle.isOn(), true); + checkstate(0, 0); + clock.forward(150); + check.equals(toggle.isOn(), false); + checkstate(0, 1); - let client2 = toggle.manipulate("test2"); - result = client(100); - check.equals(result, true); - checkstate(1, 0); - result = client2(200); - check.equals(result, true); - checkstate(0, 0); - clock.forward(150); - check.equals(toggle.isOn(), true); - checkstate(0, 0); - clock.forward(150); - check.equals(toggle.isOn(), false); - checkstate(0, 1); + let client2 = toggle.manipulate("test2"); + result = client(100); + check.equals(result, true); + checkstate(1, 0); + result = client2(200); + check.equals(result, true); + checkstate(0, 0); + clock.forward(150); + check.equals(toggle.isOn(), true); + checkstate(0, 0); + clock.forward(150); + check.equals(toggle.isOn(), false); + checkstate(0, 1); - result = client(100); - check.equals(result, true); - checkstate(1, 0); - result = client(true); - check.equals(result, true); - checkstate(0, 0); - check.equals(toggle.isOn(), true); - clock.forward(2000); - check.equals(toggle.isOn(), true); - checkstate(0, 0); - }) - }) -} \ No newline at end of file + result = client(100); + check.equals(result, true); + checkstate(1, 0); + result = client(true); + check.equals(result, true); + checkstate(0, 0); + check.equals(toggle.isOn(), true); + clock.forward(2000); + check.equals(toggle.isOn(), true); + checkstate(0, 0); + }) +}) diff --git a/src/common/Toggle.ts b/src/common/Toggle.ts index c2ef480..74fef76 100644 --- a/src/common/Toggle.ts +++ b/src/common/Toggle.ts @@ -1,93 +1,94 @@ -module TK { - /** - * Client for Toggle object, allowing to manipulate it - * - * *state* may be: - * - a boolean to require on or off - * - a number to require on for the duration in milliseconds - * - undefined to switch between on and off (based on the client state, not the toggle state) - * - * The function returns the actual state after applying the requirement - */ - export type ToggleClient = (state?: boolean | number) => boolean +import { Timer } from "./Timer" +import { add, contains, remove } from "./Tools" - /** - * A toggle between two states (on and off). - * - * A toggle will be on if at least one ToggleClient requires it to be on. - */ - export class Toggle { - private on: Function - private off: Function - private status = false - private clients: string[] = [] - private scheduled: { [client: string]: any } = {} - private timer = Timer.global +/** + * Client for Toggle object, allowing to manipulate it + * + * *state* may be: + * - a boolean to require on or off + * - a number to require on for the duration in milliseconds + * - undefined to switch between on and off (based on the client state, not the toggle state) + * + * The function returns the actual state after applying the requirement + */ +export type ToggleClient = (state?: boolean | number) => boolean - constructor(on: Function = () => null, off: Function = () => null) { - this.on = on; - this.off = off; +/** + * A toggle between two states (on and off). + * + * A toggle will be on if at least one ToggleClient requires it to be on. + */ +export class Toggle { + private on: Function + private off: Function + private status = false + private clients: string[] = [] + private scheduled: { [client: string]: any } = {} + private timer = Timer.global + + constructor(on: Function = () => null, off: Function = () => null) { + this.on = on; + this.off = off; + } + + /** + * Check if the current state is on + */ + isOn(): boolean { + return this.status; + } + + /** + * Register a client to manipulate the toggle's state + */ + manipulate(client: string): ToggleClient { + return state => { + if (this.scheduled[client]) { + this.timer.cancel(this.scheduled[client]); + this.scheduled[client] = null; + } + + if (typeof state == "undefined") { + if (contains(this.clients, client)) { + this.stop(client); + } else { + this.start(client); } - - /** - * Check if the current state is on - */ - isOn(): boolean { - return this.status; - } - - /** - * Register a client to manipulate the toggle's state - */ - manipulate(client: string): ToggleClient { - return state => { - if (this.scheduled[client]) { - this.timer.cancel(this.scheduled[client]); - this.scheduled[client] = null; - } - - if (typeof state == "undefined") { - if (contains(this.clients, client)) { - this.stop(client); - } else { - this.start(client); - } - } else if (typeof state == "number") { - if (state > 0) { - this.start(client); - this.scheduled[client] = this.timer.schedule(state, () => this.stop(client)); - } else { - this.stop(client); - } - } else if (!state) { - this.stop(client); - } else { - this.start(client); - } - return this.status; - } - } - - /** - * Start the toggle for a client (set the status *on*) - */ - private start(client: string) { - add(this.clients, client); - if (!this.status) { - this.status = true; - this.on(); - } - } - - /** - * Stop the toggle (set the status *off*) - */ - private stop(client: string) { - remove(this.clients, client); - if (this.status && this.clients.length == 0) { - this.status = false; - this.off(); - } + } else if (typeof state == "number") { + if (state > 0) { + this.start(client); + this.scheduled[client] = this.timer.schedule(state, () => this.stop(client)); + } else { + this.stop(client); } + } else if (!state) { + this.stop(client); + } else { + this.start(client); + } + return this.status; } + } + + /** + * Start the toggle for a client (set the status *on*) + */ + private start(client: string) { + add(this.clients, client); + if (!this.status) { + this.status = true; + this.on(); + } + } + + /** + * Stop the toggle (set the status *off*) + */ + private stop(client: string) { + remove(this.clients, client); + if (this.status && this.clients.length == 0) { + this.status = false; + this.off(); + } + } } diff --git a/src/common/Tools.spec.ts b/src/common/Tools.spec.ts index 9196de2..f8b8500 100644 --- a/src/common/Tools.spec.ts +++ b/src/common/Tools.spec.ts @@ -1,523 +1,521 @@ -module TK.Specs { - testing("Tools", test => { - test.case("returns boolean equivalent", check => { - check.same(bool(null), false, "null"); - check.same(bool(undefined), false, "undefined"); - - check.same(bool(false), false, "false"); - check.same(bool(true), true, "true"); - - check.same(bool(0), false, "0"); - check.same(bool(1), true, "1"); - check.same(bool(-1), true, "-1"); - - check.same(bool(""), false, "\"\""); - check.same(bool(" "), true, "\" \""); - check.same(bool("abc"), true, "\"abc\""); - - check.same(bool([]), false, "[]"); - check.same(bool([1]), true, "[1]"); - check.same(bool([null, "a"]), true, "[null, \"a\"]"); - - check.same(bool({}), false, "{}"); - check.same(bool({ a: 5 }), true, "{a: 5}"); - check.same(bool(new Timer()), true, "new Timer()"); - }); - - test.case("coalesces to a default value", check => { - check.equals(coalesce(0, 2), 0); - check.equals(coalesce(5, 2), 5); - check.equals(coalesce(null, 2), 2); - check.equals(coalesce(undefined, 2), 2); - - check.equals(coalesce("", "a"), ""); - check.equals(coalesce("b", "a"), "b"); - check.equals(coalesce(null, "a"), "a"); - check.equals(coalesce(undefined, "a"), "a"); - }); - - test.case("do basic function composition", check => { - check.equals(fmap((x: number) => x * 2, x => x + 1)(3), 8); - }); - - test.case("function composes a fallback value for null results", check => { - let f = nnf(-1, x => x > 3 ? null : x); - check.equals(f(2), 2); - check.equals(f(3), 3); - check.equals(f(4), -1); - }); - - test.case("checks for null value", check => { - let value: number | null | undefined = 5; - check.equals(nn(value), 5); - value = 0; - check.equals(nn(value), 0); - value = null; - check.throw(() => nn(value), "Null value"); - value = undefined; - check.throw(() => nn(value), "Undefined value"); - }); - - test.case("removes null values from arrays", check => { - let value: (number | null)[] = []; - check.equals(nna(value), []); - value = [1, 2]; - check.equals(nna(value), [1, 2]); - value = [1, null, 3]; - check.equals(nna(value), [1, 3]); - }); - - test.case("cast-checks objects", check => { - let obj = new RObject(); - check.equals(as(RObject, obj), obj); - check.throw(() => as(Timer, obj), new Error("Bad cast")); - }); - - test.case("compare values", check => { - check.equals(cmp(8, 5), 1); - check.same(cmp(5, 8), -1); - check.equals(cmp(5, 5), 0); - - check.equals(cmp("c", "b"), 1); - check.same(cmp("b", "c"), -1); - check.equals(cmp("b", "b"), 0); - - check.same(cmp(8, 5, true), -1); - check.equals(cmp(5, 8, true), 1); - check.equals(cmp(5, 5, true), 0); - }); - - test.case("clamp values in a range", check => { - check.equals(clamp(5, 3, 8), 5); - check.equals(clamp(1, 3, 8), 3); - check.equals(clamp(10, 3, 8), 8); - }); - - test.case("interpolates values linearly", check => { - check.equals(lerp(0, 0, 4), 0); - check.equals(lerp(0.5, 0, 4), 2); - check.equals(lerp(1, 0, 4), 4); - check.equals(lerp(2, 0, 4), 8); - check.same(lerp(-1, 0, 4), -4); - check.equals(lerp(0.5, 3, 4), 3.5); - check.equals(lerp(0.5, 3, 3), 3); - check.equals(lerp(0.5, 3, 2), 2.5); - }); - - test.case("duplicates objects", check => { - check.equals(duplicate(null), null); - check.equals(duplicate(5), 5); - check.equals(duplicate("test"), "test"); - check.equals(duplicate({ a: 4 }), { a: 4 }); - check.equals(duplicate([1, "test"]), [1, "test"]); - check.equals(duplicate(new TestSerializerObj1(6), TK.Specs), new TestSerializerObj1(6)); - let original = new TestRObject(4); - check.equals(duplicate(original, TK.Specs), original); - check.notsame(duplicate(original, TK.Specs), original); - }); - - test.case("copies arrays", check => { - var array = [1, 2, "test", null, { "a": 5 }]; - var copied = acopy(array); - - check.equals(copied, array); - check.notsame(copied, array); - check.same(copied[4], array[4]); - - check.equals(array[2], "test"); - check.equals(copied[2], "test"); - array[2] = "notest"; - check.equals(array[2], "notest"); - check.equals(copied[2], "test"); - copied[2] = "ok"; - check.equals(array[2], "notest"); - check.equals(copied[2], "ok"); - - check.equals(array.length, 5); - check.equals(copied.length, 5); - remove(copied, 2); - check.equals(array.length, 5); - check.equals(copied.length, 4); - }); - - test.case("iterates through sorted arrays", check => { - var result: number[] = []; - itersorted([1, 2, 3], item => item, item => result.push(item)); - check.equals(result, [1, 2, 3]); - - result = []; - itersorted([1, 2, 3], item => -item, item => result.push(item)); - check.equals(result, [3, 2, 1]); - }); - - test.case("checks if an array contains an item", check => { - check.equals(contains([], 5), false); - - check.equals(contains([3, 5, 8], 5), true); - check.equals(contains([3, 5, 8], 4), false); - - check.equals(contains([5, 5, 5], 5), true); - - check.equals(contains([3, null, 8], null), true); - - check.equals(contains(["a", "b"], "b"), true); - check.equals(contains(["a", "b"], "c"), false); - }); - - test.case("capitalizes strings", check => { - check.equals(capitalize("test"), "Test"); - check.equals(capitalize("test second"), "Test second"); - }); - - test.case("produces range of numbers", check => { - check.equals(range(-1), []); - check.equals(range(0), []); - check.equals(range(1), [0]); - check.equals(range(2), [0, 1]); - check.equals(range(5), [0, 1, 2, 3, 4]); - }); - - test.case("zips arrays", check => { - check.equals(zip([], []), []); - check.equals(zip([], [1]), []); - check.equals(zip([0], [1]), [[0, 1]]); - check.equals(zip([0, 2, 4], [1, 3]), [[0, 1], [2, 3]]); - check.equals(zip([0, 1], ["a", "b"]), [[0, "a"], [1, "b"]]); - }); - - test.case("unzips arrays", check => { - check.equals(unzip([]), [[], []]); - check.equals(unzip([[1, "a"]]), [[1], ["a"]]); - check.equals(unzip([[1, "a"], [2, "b"]]), [[1, 2], ["a", "b"]]); - }); - - test.case("partitions arrays by a predicate", check => { - check.equals(binpartition([], (i: number) => i % 2 == 0), [[], []]); - check.equals(binpartition([1, 2, 3, 4], i => i % 2 == 0), [[2, 4], [1, 3]]); - }); - - test.case("produces neighbor tuples from array", check => { - check.equals(neighbors([]), []); - check.equals(neighbors([1]), []); - check.equals(neighbors([1, 2]), [[1, 2]]); - check.equals(neighbors([1, 2, 3]), [[1, 2], [2, 3]]); - check.equals(neighbors([1, 2, 3, 4]), [[1, 2], [2, 3], [3, 4]]); - - check.equals(neighbors([], true), []); - check.equals(neighbors([1], true), [[1, 1]]); - check.equals(neighbors([1, 2], true), [[1, 2], [2, 1]]); - check.equals(neighbors([1, 2, 3], true), [[1, 2], [2, 3], [3, 1]]); - check.equals(neighbors([1, 2, 3, 4], true), [[1, 2], [2, 3], [3, 4], [4, 1]]); - }); - - test.case("filters list with type guards", check => { - let result = tfilter(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number"); - check.equals(result, [1, 2]); - - let o1 = new RObject(); - let o2 = new RObject(); - let o3 = new RObjectContainer(); - check.equals(cfilter([1, "a", o1, 2, o2, o3, "b"], RObject), [o1, o2]); - }); - - test.case("flattens lists of lists", check => { - check.equals(flatten([]), []); - check.equals(flatten([[]]), []); - check.equals(flatten([[], []]), []); - check.equals(flatten([[1], []]), [1]); - check.equals(flatten([[], [1]]), [1]); - check.equals(flatten([[1], [2]]), [1, 2]); - check.equals(flatten([[1, 2], [3, 4], [], [5]]), [1, 2, 3, 4, 5]); - }); - - test.case("counts items in array", check => { - check.equals(counter([]), []); - check.equals(counter(["a"]), [["a", 1]]); - check.equals(counter(["a", "b"]), [["a", 1], ["b", 1]]); - check.equals(counter(["a", "b", "a"]), [["a", 2], ["b", 1]]); - }); - - test.case("find the first array item to pass a predicate", check => { - check.equals(first([1, 2, 3], i => i % 2 == 0), 2); - check.equals(first([1, 2, 3], i => i % 4 == 0), null); - - check.equals(any([1, 2, 3], i => i % 2 == 0), true); - check.equals(any([1, 2, 3], i => i % 4 == 0), false); - }); - - test.case("creates a simple iterator over an array", check => { - let i = iterator([1, 2, 3]); - check.equals(i(), 1); - check.equals(i(), 2); - check.equals(i(), 3); - check.equals(i(), null); - check.equals(i(), null); - - i = iterator([]); - check.equals(i(), null); - check.equals(i(), null); - }); - - test.case("iterates an object keys and values", check => { - var obj = { - "a": 1, - "c": [2.5], - "b": null - }; - check.equals(keys(obj), ["a", "c", "b"]); - check.equals(values(obj), [1, [2.5], null]); - var result: { [key: string]: any } = {}; - iteritems(obj, (key, value) => { result[key] = value; }); - check.equals(result, obj); - - check.equals(dict(items(obj)), obj); - }); - - test.case("gets an enum values", check => { - enum Test { - ZERO, - ONE, - TWO - }; - - var result: any[] = []; - iterenum(Test, item => result.push(item)); - check.equals(result, [0, 1, 2]); - check.equals(result, [Test.ZERO, Test.ONE, Test.TWO]); - - check.equals(enumvalues(Test), [0, 1, 2]); - check.equals(enumvalues(Test), [Test.ZERO, Test.ONE, Test.TWO]); - }); - - test.case("create a dict from an array of couples", check => { - check.equals(dict([]), {}); - check.equals(dict([["5", 3], ["4", 1], ["5", 8]]), { "5": 8, "4": 1 }); - }); - - test.case("create an index from an array", check => { - check.equals(index([2, 3, 4], i => (i - 1).toString()), { "1": 2, "2": 3, "3": 4 }); - }); - - test.case("add an item in an array", check => { - var result; - var array = [1]; - - result = add(array, 8); - check.equals(array, [1, 8]); - check.equals(result, true); - - result = add(array, 2); - check.equals(array, [1, 8, 2]); - check.equals(result, true); - - result = add(array, 8); - check.equals(array, [1, 8, 2]); - check.equals(result, false); - }); - - test.case("removes an item from an array", check => { - var array = [1, 2, 3]; - var result = remove(array, 1); - check.equals(array, [2, 3]); - check.equals(result, true); - result = remove(array, 1); - check.equals(array, [2, 3]); - check.equals(result, false); - result = remove(array, 2); - check.equals(array, [3]); - check.equals(result, true); - result = remove(array, 3); - check.equals(array, []); - check.equals(result, true); - result = remove(array, 3); - check.equals(array, []); - check.equals(result, false); - }); - - test.case("checks objects equality", check => { - check.equals(equals({}, {}), true); - check.equals(equals({ "a": 1 }, { "a": 1 }), true); - check.equals(equals({ "a": 1 }, { "a": 2 }), false); - check.equals(equals({ "a": 1 }, { "b": 1 }), false); - check.equals(equals({ "a": 1 }, { "a": null }), false); - }); - - test.case("combinate filters", check => { - var filter = andfilter((item: number) => item > 5, (item: number) => item < 12); - check.equals(filter(4), false); - check.equals(filter(5), false); - check.equals(filter(6), true); - check.equals(filter(8), true); - check.equals(filter(11), true); - check.equals(filter(12), false); - check.equals(filter(13), false); - }); - - test.case("get a class name", check => { - class Test { - } - var a = new Test(); - check.equals(classname(a), "Test"); - }); - - test.case("find lowest item of an array", check => { - check.equals(lowest(["aaa", "b", "cc"], s => s.length), "b"); - }); - - test.case("binds callbacks", check => { - class Test { - prop = 5; - meth() { - return this.prop + 1; - } - } - var inst = new Test(); - var double = (getter: () => number): number => getter() * 2; - check.throw(() => double(inst.meth)); - check.equals(double(bound(inst, "meth")), 12); - }); - - test.case("computes progress between two boundaries", check => { - check.equals(progress(-1.0, 0.0, 1.0), 0.0); - check.equals(progress(0.0, 0.0, 1.0), 0.0); - check.equals(progress(0.4, 0.0, 1.0), 0.4); - check.equals(progress(1.8, 0.0, 1.0), 1.0); - check.equals(progress(1.5, 0.5, 2.5), 0.5); - }); - - test.case("copies full javascript objects", check => { - class TestObj { - a: string; - b: any; - constructor() { - this.a = "test"; - this.b = { c: 5.1, d: ["unit", "test", 5] }; - } - get(): string { - return this.a; - } - } - - var ini = new TestObj(); - - var cop = copy(ini); - - check.notsame(cop, ini); - check.equals(cop, ini); - - check.equals(cop.get(), "test"); - }); - - test.case("merges objects", check => { - check.equals(merge({}, {}), {}); - check.equals(merge({ a: 1 }, { b: 2 }), { a: 1, b: 2 }); - check.equals(merge({ a: 1 }, { a: 3, b: 2 }), { a: 3, b: 2 }); - check.equals(merge({ a: 1, b: 2 }, { a: undefined }), { a: undefined, b: 2 }); - }); - - test.case("crawls through objects", check => { - var obj = { - "a": 1, - "b": "test", - "c": { - "a": [2, "thing", { "a": 3, "b": {} }], - "b": null, - "c": undefined, - "d": false - } - }; - /*(obj).jasmineToString = () => "obj1"; - (obj.c).jasmineToString = () => "obj2"; - (obj.c.a[2]).jasmineToString = () => "obj3"; - (obj.c.a[2].b).jasmineToString = () => "obj4"; - (obj.c.a).jasmineToString = () => "array1";*/ - - var crawled: any[] = []; - crawl(obj, val => crawled.push(val)); - check.equals(crawled, [obj, 1, "test", obj.c, obj.c.a, 2, "thing", obj.c.a[2], 3, obj.c.a[2].b, false]); - check.equals(obj.a, 1); - - // replace mode - crawl(obj, val => typeof val == "number" ? 5 : val, true); - check.equals(obj, { a: 5, b: "test", c: { a: [5, "thing", { a: 5, b: {} }], b: null, c: undefined, d: false } }); - }); - - test.case("get minimal item of an array", check => { - check.equals(min([5, 1, 8]), 1); - }); - - test.case("get maximal item of an array", check => { - check.equals(max([5, 12, 8]), 12); - }); - - test.case("get sum of an array", check => { - check.equals(sum([5, 1, 8]), 14); - }); - - test.case("get average of an array", check => { - check.equals(avg([4, 2, 9]), 5); - }); - - test.case("converts to same sign", check => { - check.equals(samesign(2, 1), 2); - check.equals(samesign(2, -1), -2); - check.equals(samesign(-2, 1), 2); - check.equals(samesign(-2, -1), -2); - }); - - test.case("sorts an array", check => { - let base = ["aa", "bbb", "c", "dddd"]; - check.equals(sorted(base, (a, b) => cmp(a.length, b.length)), ["c", "aa", "bbb", "dddd"]); - check.equals(base, ["aa", "bbb", "c", "dddd"]); - }); - - test.case("sorts an array, with function applied to each element", check => { - check.equals(sortedBy([-8, 4, -2, 6], Math.abs), [-2, 4, 6, -8]); - check.equals(sortedBy([-8, 4, -2, 6], Math.abs, true), [-8, 6, 4, -2]); - }); - - test.case("get minimal item of an array, with function applied to each element", check => { - check.equals(minBy([-8, 4, -2, 6], Math.abs), -2); - }); - - test.case("get maximal item of an array, with function applied to each element", check => { - check.equals(maxBy([-8, 4, -2, 6], Math.abs), -8); - }); - - test.case("filter out duplicates in array", check => { - check.equals(unique([]), []); - check.equals(unique([1, 2, 3]), [1, 2, 3]); - check.equals(unique([1, 2, 3, 2, 1]), [1, 2, 3]); - }); - - test.case("get the union between two arrays", check => { - check.equals(union([], []), []); - check.equals(union([], [5]), [5]); - check.equals(union([4], []), [4]); - check.equals(union([4], [5]), [4, 5]); - check.equals(union([1, 2, 4, 8], [3, 2, 8, 7]), [1, 2, 4, 8, 3, 7]); - }); - - test.case("get the difference between two arrays", check => { - check.equals(difference([], []), []); - check.equals(difference([], [5]), []); - check.equals(difference([1, 2, 4, 8], [2, 8, 7]), [1, 4]); - }); - - test.case("get the intersection of two arrays", check => { - check.equals(intersection([], []), []); - check.equals(intersection([], [5]), []); - check.equals(intersection([4], []), []); - check.equals(intersection([6], [7]), []); - check.equals(intersection([1, 8, 2], [2, 8, 4]), [8, 2]); - }); - - test.case("get the disjunct union of two arrays", check => { - check.equals(disjunctunion([], []), []); - check.equals(disjunctunion([], [5]), [5]); - check.equals(disjunctunion([4], []), [4]); - check.equals(disjunctunion([6], [7]), [6, 7]); - check.equals(disjunctunion([1, 8, 2], [2, 8, 4]), [1, 4]); - }); - }); -} +testing("Tools", test => { + test.case("returns boolean equivalent", check => { + check.same(bool(null), false, "null"); + check.same(bool(undefined), false, "undefined"); + + check.same(bool(false), false, "false"); + check.same(bool(true), true, "true"); + + check.same(bool(0), false, "0"); + check.same(bool(1), true, "1"); + check.same(bool(-1), true, "-1"); + + check.same(bool(""), false, "\"\""); + check.same(bool(" "), true, "\" \""); + check.same(bool("abc"), true, "\"abc\""); + + check.same(bool([]), false, "[]"); + check.same(bool([1]), true, "[1]"); + check.same(bool([null, "a"]), true, "[null, \"a\"]"); + + check.same(bool({}), false, "{}"); + check.same(bool({ a: 5 }), true, "{a: 5}"); + check.same(bool(new Timer()), true, "new Timer()"); + }); + + test.case("coalesces to a default value", check => { + check.equals(coalesce(0, 2), 0); + check.equals(coalesce(5, 2), 5); + check.equals(coalesce(null, 2), 2); + check.equals(coalesce(undefined, 2), 2); + + check.equals(coalesce("", "a"), ""); + check.equals(coalesce("b", "a"), "b"); + check.equals(coalesce(null, "a"), "a"); + check.equals(coalesce(undefined, "a"), "a"); + }); + + test.case("do basic function composition", check => { + check.equals(fmap((x: number) => x * 2, x => x + 1)(3), 8); + }); + + test.case("function composes a fallback value for null results", check => { + let f = nnf(-1, x => x > 3 ? null : x); + check.equals(f(2), 2); + check.equals(f(3), 3); + check.equals(f(4), -1); + }); + + test.case("checks for null value", check => { + let value: number | null | undefined = 5; + check.equals(nn(value), 5); + value = 0; + check.equals(nn(value), 0); + value = null; + check.throw(() => nn(value), "Null value"); + value = undefined; + check.throw(() => nn(value), "Undefined value"); + }); + + test.case("removes null values from arrays", check => { + let value: (number | null)[] = []; + check.equals(nna(value), []); + value = [1, 2]; + check.equals(nna(value), [1, 2]); + value = [1, null, 3]; + check.equals(nna(value), [1, 3]); + }); + + test.case("cast-checks objects", check => { + let obj = new RObject(); + check.equals(as(RObject, obj), obj); + check.throw(() => as(Timer, obj), new Error("Bad cast")); + }); + + test.case("compare values", check => { + check.equals(cmp(8, 5), 1); + check.same(cmp(5, 8), -1); + check.equals(cmp(5, 5), 0); + + check.equals(cmp("c", "b"), 1); + check.same(cmp("b", "c"), -1); + check.equals(cmp("b", "b"), 0); + + check.same(cmp(8, 5, true), -1); + check.equals(cmp(5, 8, true), 1); + check.equals(cmp(5, 5, true), 0); + }); + + test.case("clamp values in a range", check => { + check.equals(clamp(5, 3, 8), 5); + check.equals(clamp(1, 3, 8), 3); + check.equals(clamp(10, 3, 8), 8); + }); + + test.case("interpolates values linearly", check => { + check.equals(lerp(0, 0, 4), 0); + check.equals(lerp(0.5, 0, 4), 2); + check.equals(lerp(1, 0, 4), 4); + check.equals(lerp(2, 0, 4), 8); + check.same(lerp(-1, 0, 4), -4); + check.equals(lerp(0.5, 3, 4), 3.5); + check.equals(lerp(0.5, 3, 3), 3); + check.equals(lerp(0.5, 3, 2), 2.5); + }); + + test.case("duplicates objects", check => { + check.equals(duplicate(null), null); + check.equals(duplicate(5), 5); + check.equals(duplicate("test"), "test"); + check.equals(duplicate({ a: 4 }), { a: 4 }); + check.equals(duplicate([1, "test"]), [1, "test"]); + check.equals(duplicate(new TestSerializerObj1(6), TK.Specs), new TestSerializerObj1(6)); + let original = new TestRObject(4); + check.equals(duplicate(original, TK.Specs), original); + check.notsame(duplicate(original, TK.Specs), original); + }); + + test.case("copies arrays", check => { + var array = [1, 2, "test", null, { "a": 5 }]; + var copied = acopy(array); + + check.equals(copied, array); + check.notsame(copied, array); + check.same(copied[4], array[4]); + + check.equals(array[2], "test"); + check.equals(copied[2], "test"); + array[2] = "notest"; + check.equals(array[2], "notest"); + check.equals(copied[2], "test"); + copied[2] = "ok"; + check.equals(array[2], "notest"); + check.equals(copied[2], "ok"); + + check.equals(array.length, 5); + check.equals(copied.length, 5); + remove(copied, 2); + check.equals(array.length, 5); + check.equals(copied.length, 4); + }); + + test.case("iterates through sorted arrays", check => { + var result: number[] = []; + itersorted([1, 2, 3], item => item, item => result.push(item)); + check.equals(result, [1, 2, 3]); + + result = []; + itersorted([1, 2, 3], item => -item, item => result.push(item)); + check.equals(result, [3, 2, 1]); + }); + + test.case("checks if an array contains an item", check => { + check.equals(contains([], 5), false); + + check.equals(contains([3, 5, 8], 5), true); + check.equals(contains([3, 5, 8], 4), false); + + check.equals(contains([5, 5, 5], 5), true); + + check.equals(contains([3, null, 8], null), true); + + check.equals(contains(["a", "b"], "b"), true); + check.equals(contains(["a", "b"], "c"), false); + }); + + test.case("capitalizes strings", check => { + check.equals(capitalize("test"), "Test"); + check.equals(capitalize("test second"), "Test second"); + }); + + test.case("produces range of numbers", check => { + check.equals(range(-1), []); + check.equals(range(0), []); + check.equals(range(1), [0]); + check.equals(range(2), [0, 1]); + check.equals(range(5), [0, 1, 2, 3, 4]); + }); + + test.case("zips arrays", check => { + check.equals(zip([], []), []); + check.equals(zip([], [1]), []); + check.equals(zip([0], [1]), [[0, 1]]); + check.equals(zip([0, 2, 4], [1, 3]), [[0, 1], [2, 3]]); + check.equals(zip([0, 1], ["a", "b"]), [[0, "a"], [1, "b"]]); + }); + + test.case("unzips arrays", check => { + check.equals(unzip([]), [[], []]); + check.equals(unzip([[1, "a"]]), [[1], ["a"]]); + check.equals(unzip([[1, "a"], [2, "b"]]), [[1, 2], ["a", "b"]]); + }); + + test.case("partitions arrays by a predicate", check => { + check.equals(binpartition([], (i: number) => i % 2 == 0), [[], []]); + check.equals(binpartition([1, 2, 3, 4], i => i % 2 == 0), [[2, 4], [1, 3]]); + }); + + test.case("produces neighbor tuples from array", check => { + check.equals(neighbors([]), []); + check.equals(neighbors([1]), []); + check.equals(neighbors([1, 2]), [[1, 2]]); + check.equals(neighbors([1, 2, 3]), [[1, 2], [2, 3]]); + check.equals(neighbors([1, 2, 3, 4]), [[1, 2], [2, 3], [3, 4]]); + + check.equals(neighbors([], true), []); + check.equals(neighbors([1], true), [[1, 1]]); + check.equals(neighbors([1, 2], true), [[1, 2], [2, 1]]); + check.equals(neighbors([1, 2, 3], true), [[1, 2], [2, 3], [3, 1]]); + check.equals(neighbors([1, 2, 3, 4], true), [[1, 2], [2, 3], [3, 4], [4, 1]]); + }); + + test.case("filters list with type guards", check => { + let result = tfilter(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number"); + check.equals(result, [1, 2]); + + let o1 = new RObject(); + let o2 = new RObject(); + let o3 = new RObjectContainer(); + check.equals(cfilter([1, "a", o1, 2, o2, o3, "b"], RObject), [o1, o2]); + }); + + test.case("flattens lists of lists", check => { + check.equals(flatten([]), []); + check.equals(flatten([[]]), []); + check.equals(flatten([[], []]), []); + check.equals(flatten([[1], []]), [1]); + check.equals(flatten([[], [1]]), [1]); + check.equals(flatten([[1], [2]]), [1, 2]); + check.equals(flatten([[1, 2], [3, 4], [], [5]]), [1, 2, 3, 4, 5]); + }); + + test.case("counts items in array", check => { + check.equals(counter([]), []); + check.equals(counter(["a"]), [["a", 1]]); + check.equals(counter(["a", "b"]), [["a", 1], ["b", 1]]); + check.equals(counter(["a", "b", "a"]), [["a", 2], ["b", 1]]); + }); + + test.case("find the first array item to pass a predicate", check => { + check.equals(first([1, 2, 3], i => i % 2 == 0), 2); + check.equals(first([1, 2, 3], i => i % 4 == 0), null); + + check.equals(any([1, 2, 3], i => i % 2 == 0), true); + check.equals(any([1, 2, 3], i => i % 4 == 0), false); + }); + + test.case("creates a simple iterator over an array", check => { + let i = iterator([1, 2, 3]); + check.equals(i(), 1); + check.equals(i(), 2); + check.equals(i(), 3); + check.equals(i(), null); + check.equals(i(), null); + + i = iterator([]); + check.equals(i(), null); + check.equals(i(), null); + }); + + test.case("iterates an object keys and values", check => { + var obj = { + "a": 1, + "c": [2.5], + "b": null + }; + check.equals(keys(obj), ["a", "c", "b"]); + check.equals(values(obj), [1, [2.5], null]); + var result: { [key: string]: any } = {}; + iteritems(obj, (key, value) => { result[key] = value; }); + check.equals(result, obj); + + check.equals(dict(items(obj)), obj); + }); + + test.case("gets an enum values", check => { + enum Test { + ZERO, + ONE, + TWO + }; + + var result: any[] = []; + iterenum(Test, item => result.push(item)); + check.equals(result, [0, 1, 2]); + check.equals(result, [Test.ZERO, Test.ONE, Test.TWO]); + + check.equals(enumvalues(Test), [0, 1, 2]); + check.equals(enumvalues(Test), [Test.ZERO, Test.ONE, Test.TWO]); + }); + + test.case("create a dict from an array of couples", check => { + check.equals(dict([]), {}); + check.equals(dict([["5", 3], ["4", 1], ["5", 8]]), { "5": 8, "4": 1 }); + }); + + test.case("create an index from an array", check => { + check.equals(index([2, 3, 4], i => (i - 1).toString()), { "1": 2, "2": 3, "3": 4 }); + }); + + test.case("add an item in an array", check => { + var result; + var array = [1]; + + result = add(array, 8); + check.equals(array, [1, 8]); + check.equals(result, true); + + result = add(array, 2); + check.equals(array, [1, 8, 2]); + check.equals(result, true); + + result = add(array, 8); + check.equals(array, [1, 8, 2]); + check.equals(result, false); + }); + + test.case("removes an item from an array", check => { + var array = [1, 2, 3]; + var result = remove(array, 1); + check.equals(array, [2, 3]); + check.equals(result, true); + result = remove(array, 1); + check.equals(array, [2, 3]); + check.equals(result, false); + result = remove(array, 2); + check.equals(array, [3]); + check.equals(result, true); + result = remove(array, 3); + check.equals(array, []); + check.equals(result, true); + result = remove(array, 3); + check.equals(array, []); + check.equals(result, false); + }); + + test.case("checks objects equality", check => { + check.equals(equals({}, {}), true); + check.equals(equals({ "a": 1 }, { "a": 1 }), true); + check.equals(equals({ "a": 1 }, { "a": 2 }), false); + check.equals(equals({ "a": 1 }, { "b": 1 }), false); + check.equals(equals({ "a": 1 }, { "a": null }), false); + }); + + test.case("combinate filters", check => { + var filter = andfilter((item: number) => item > 5, (item: number) => item < 12); + check.equals(filter(4), false); + check.equals(filter(5), false); + check.equals(filter(6), true); + check.equals(filter(8), true); + check.equals(filter(11), true); + check.equals(filter(12), false); + check.equals(filter(13), false); + }); + + test.case("get a class name", check => { + class Test { + } + var a = new Test(); + check.equals(classname(a), "Test"); + }); + + test.case("find lowest item of an array", check => { + check.equals(lowest(["aaa", "b", "cc"], s => s.length), "b"); + }); + + test.case("binds callbacks", check => { + class Test { + prop = 5; + meth() { + return this.prop + 1; + } + } + var inst = new Test(); + var double = (getter: () => number): number => getter() * 2; + check.throw(() => double(inst.meth)); + check.equals(double(bound(inst, "meth")), 12); + }); + + test.case("computes progress between two boundaries", check => { + check.equals(progress(-1.0, 0.0, 1.0), 0.0); + check.equals(progress(0.0, 0.0, 1.0), 0.0); + check.equals(progress(0.4, 0.0, 1.0), 0.4); + check.equals(progress(1.8, 0.0, 1.0), 1.0); + check.equals(progress(1.5, 0.5, 2.5), 0.5); + }); + + test.case("copies full javascript objects", check => { + class TestObj { + a: string; + b: any; + constructor() { + this.a = "test"; + this.b = { c: 5.1, d: ["unit", "test", 5] }; + } + get(): string { + return this.a; + } + } + + var ini = new TestObj(); + + var cop = copy(ini); + + check.notsame(cop, ini); + check.equals(cop, ini); + + check.equals(cop.get(), "test"); + }); + + test.case("merges objects", check => { + check.equals(merge({}, {}), {}); + check.equals(merge({ a: 1 }, { b: 2 }), { a: 1, b: 2 }); + check.equals(merge({ a: 1 }, { a: 3, b: 2 }), { a: 3, b: 2 }); + check.equals(merge({ a: 1, b: 2 }, { a: undefined }), { a: undefined, b: 2 }); + }); + + test.case("crawls through objects", check => { + var obj = { + "a": 1, + "b": "test", + "c": { + "a": [2, "thing", { "a": 3, "b": {} }], + "b": null, + "c": undefined, + "d": false + } + }; + /*(obj).jasmineToString = () => "obj1"; + (obj.c).jasmineToString = () => "obj2"; + (obj.c.a[2]).jasmineToString = () => "obj3"; + (obj.c.a[2].b).jasmineToString = () => "obj4"; + (obj.c.a).jasmineToString = () => "array1";*/ + + var crawled: any[] = []; + crawl(obj, val => crawled.push(val)); + check.equals(crawled, [obj, 1, "test", obj.c, obj.c.a, 2, "thing", obj.c.a[2], 3, obj.c.a[2].b, false]); + check.equals(obj.a, 1); + + // replace mode + crawl(obj, val => typeof val == "number" ? 5 : val, true); + check.equals(obj, { a: 5, b: "test", c: { a: [5, "thing", { a: 5, b: {} }], b: null, c: undefined, d: false } }); + }); + + test.case("get minimal item of an array", check => { + check.equals(min([5, 1, 8]), 1); + }); + + test.case("get maximal item of an array", check => { + check.equals(max([5, 12, 8]), 12); + }); + + test.case("get sum of an array", check => { + check.equals(sum([5, 1, 8]), 14); + }); + + test.case("get average of an array", check => { + check.equals(avg([4, 2, 9]), 5); + }); + + test.case("converts to same sign", check => { + check.equals(samesign(2, 1), 2); + check.equals(samesign(2, -1), -2); + check.equals(samesign(-2, 1), 2); + check.equals(samesign(-2, -1), -2); + }); + + test.case("sorts an array", check => { + let base = ["aa", "bbb", "c", "dddd"]; + check.equals(sorted(base, (a, b) => cmp(a.length, b.length)), ["c", "aa", "bbb", "dddd"]); + check.equals(base, ["aa", "bbb", "c", "dddd"]); + }); + + test.case("sorts an array, with function applied to each element", check => { + check.equals(sortedBy([-8, 4, -2, 6], Math.abs), [-2, 4, 6, -8]); + check.equals(sortedBy([-8, 4, -2, 6], Math.abs, true), [-8, 6, 4, -2]); + }); + + test.case("get minimal item of an array, with function applied to each element", check => { + check.equals(minBy([-8, 4, -2, 6], Math.abs), -2); + }); + + test.case("get maximal item of an array, with function applied to each element", check => { + check.equals(maxBy([-8, 4, -2, 6], Math.abs), -8); + }); + + test.case("filter out duplicates in array", check => { + check.equals(unique([]), []); + check.equals(unique([1, 2, 3]), [1, 2, 3]); + check.equals(unique([1, 2, 3, 2, 1]), [1, 2, 3]); + }); + + test.case("get the union between two arrays", check => { + check.equals(union([], []), []); + check.equals(union([], [5]), [5]); + check.equals(union([4], []), [4]); + check.equals(union([4], [5]), [4, 5]); + check.equals(union([1, 2, 4, 8], [3, 2, 8, 7]), [1, 2, 4, 8, 3, 7]); + }); + + test.case("get the difference between two arrays", check => { + check.equals(difference([], []), []); + check.equals(difference([], [5]), []); + check.equals(difference([1, 2, 4, 8], [2, 8, 7]), [1, 4]); + }); + + test.case("get the intersection of two arrays", check => { + check.equals(intersection([], []), []); + check.equals(intersection([], [5]), []); + check.equals(intersection([4], []), []); + check.equals(intersection([6], [7]), []); + check.equals(intersection([1, 8, 2], [2, 8, 4]), [8, 2]); + }); + + test.case("get the disjunct union of two arrays", check => { + check.equals(disjunctunion([], []), []); + check.equals(disjunctunion([], [5]), [5]); + check.equals(disjunctunion([4], []), [4]); + check.equals(disjunctunion([6], [7]), [6, 7]); + check.equals(disjunctunion([1, 8, 2], [2, 8, 4]), [1, 4]); + }); +}); diff --git a/src/common/Tools.ts b/src/common/Tools.ts index 0e1c6e4..2afe319 100644 --- a/src/common/Tools.ts +++ b/src/common/Tools.ts @@ -1,640 +1,637 @@ +import { Serializer } from "./Serializer"; + /** - * Various utility functions. + * Functions that does nothing (useful for default callbacks) */ -module TK { - /** - * Functions that does nothing (useful for default callbacks) - */ - export function nop(): void { - } - - /** - * Identity function (returns the sole argument untouched) - */ - export function identity(input: T): T { - return input; - } - - /** - * Check a value for a boolean equivalent - */ - export function bool(value: T | null | undefined): value is T; - export function bool(value: any): boolean { - if (!value) { - return false; - } else if (typeof value == "object") { - return Object.keys(value).length > 0; - } else { - return true; - } - } - - /** - * Return a default value if the given one is undefined - */ - export function coalesce(value: string | null | undefined, fallback: string): string; - export function coalesce(value: number | null | undefined, fallback: number): number; - export function coalesce(value: T | null | undefined, fallback: T): T { - if (typeof value == "undefined" || value === null) { - return fallback; - } else { - return value; - } - } - - /** - * Check for an object being an instance of a type, an returns casted version - * - * Throws an error on failure to cast - */ - export function as(classref: { new(...args: any[]): T }, obj: any): T { - if (obj instanceof classref) { - return obj; - } else { - console.error("Bad cast", obj, classref); - throw new Error("Bad cast"); - } - } - - /** - * Apply a functor on the result of another function - */ - export function fmap(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V { - // TODO variadic typing, as soon as supported by typescript - return (...args) => g(f(...args)); - } - - /** - * Apply a default value to nulls or undefineds returned by a function - */ - export function nnf(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T { - return fmap(val => val === null ? fallback : val, f); - } - - /** - * Check if a value if null, throwing an exception if its the case - */ - export function nn(value: T | null | undefined): T { - if (value === null) { - throw new Error("Null value"); - } else if (typeof value == "undefined") { - throw new Error("Undefined value"); - } else { - return value; - } - } - - /** - * Remove null values from an array - */ - export function nna(array: (T | null)[]): T[] { - return array.filter(item => item !== null); - } - - /** - * Compare operator, that can be used in sort() calls. - */ - export function cmp(a: any, b: any, reverse = false): number { - if (a > b) { - return reverse ? -1 : 1; - } else if (a < b) { - return reverse ? 1 : -1; - } else { - return 0; - } - } - - /** - * Clamp a value in a range. - */ - export function clamp(value: T, min: T, max: T): T { - if (value < min) { - return min; - } else if (value > max) { - return max; - } else { - return value; - } - } - - /** - * Perform a linear interpolation between two values (factor is between 0 and 1). - */ - export function lerp(factor: number, min: number, max: number): number { - return min + (max - min) * factor; - } - - /** - * Make a deep copy of any object. - * - * Serializer is used for this, and needs a namespace to work. - * - * Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness. - */ - export function duplicate(obj: T, namespace: Object = TK): T { - let serializer = new Serializer(namespace); - let serialized = serializer.serialize({ dat: obj }); - return serializer.unserialize(serialized).dat; - } - - /** - * Make a shallow copy of an array. - */ - export function acopy(input: T[]): T[] { - return input.slice(); - } - - /** - * Call a function for each member of an array, sorted by a key. - */ - export function itersorted(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void { - var array = acopy(input); - array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2))); - array.forEach(callback); - } - - /** - * Capitalize the first letter of an input string. - */ - export function capitalize(input: string): string { - return input.charAt(0).toLocaleUpperCase() + input.slice(1); - }; - - /** - * Check if an array contains an item. - */ - export function contains(array: T[], item: T): boolean { - return array.indexOf(item) >= 0; - } - - /** - * Produce an n-sized array, with integers counting from 0 - */ - export function range(n: number): number[] { - var result: number[] = []; - for (var i = 0; i < n; i++) { - result.push(i); - } - return result; - } - - /** - * Produce an array of couples, build from the common length of two arrays - */ - export function zip(array1: T1[], array2: T2[]): [T1, T2][] { - var result: [T1, T2][] = []; - var n = (array1.length > array2.length) ? array2.length : array1.length; - for (var i = 0; i < n; i++) { - result.push([array1[i], array2[i]]); - } - return result; - } - - /** - * Produce two arrays, build from an array of couples - */ - export function unzip(array: [T1, T2][]): [T1[], T2[]] { - return [array.map(x => x[0]), array.map(x => x[1])]; - } - - /** - * Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it - */ - export function binpartition(array: T[], predicate: (item: T) => boolean): [T[], T[]] { - let pass: T[] = []; - let fail: T[] = []; - array.forEach(item => (predicate(item) ? pass : fail).push(item)); - return [pass, fail]; - } - - /** - * Yields the neighbors tuple list - */ - export function neighbors(array: T[], wrap = false): [T, T][] { - var result: [T, T][] = []; - if (array.length > 0) { - var previous = array[0]; - for (var i = 1; i < array.length; i++) { - result.push([previous, array[i]]); - previous = array[i]; - } - if (wrap) { - result.push([previous, array[0]]); - } - return result; - } else { - return []; - } - } - - /** - * Type filter, to return a list of instances of a given type - */ - export function tfilter(array: any[], filter: (item: any) => item is T): T[] { - return array.filter(filter); - } - - /** - * Class filter, to return a list of instances of a given type - */ - export function cfilter(array: any[], classref: { new(...args: any[]): T }): T[] { - return array.filter((item): item is T => item instanceof classref); - } - - /** - * Flatten a list of lists - */ - export function flatten(array: T[][]): T[] { - return array.reduce((a, b) => a.concat(b), []); - } - - /** - * Count each element in an array - */ - export function counter(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] { - var result: [T, number][] = []; - array.forEach(item => { - var found = first(result, iter => equals(iter[0], item)); - if (found) { - found[1]++; - } else { - result.push([item, 1]); - } - }); - return result; - } - - /** - * Return the first element of the array that matches the predicate, null if not found - */ - export function first(array: T[], predicate: (item: T) => boolean): T | null { - for (var i = 0; i < array.length; i++) { - if (predicate(array[i])) { - return array[i]; - } - } - return null; - } - - /** - * Return whether if any element in the array matches the predicate - */ - export function any(array: T[], predicate: (item: T) => boolean): boolean { - return first(array, predicate) != null; - } - - /** - * Return an iterator over an array - * - * An iterator is a function yielding the next value each time, until the end of array where it yields null. - * - * For more powerful iterators, see Iterators - */ - export function iterator(array: T[]): () => T | null { - let i = 0; - return () => (i < array.length) ? array[i++] : null; - } - - /** - * Iterate a list of (key, value) in an object. - */ - export function iteritems(obj: { [key: string]: T }, func: (key: string, value: T) => void) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - func(key, obj[key]); - } - } - } - - /** - * Transform an dictionary object to a list of couples (key, value). - */ - export function items(obj: { [key: string]: T }): [string, T][] { - let result: [string, T][] = []; - iteritems(obj, (key, value) => result.push([key, value])); - return result; - } - - /** - * Return the list of keys from an object. - */ - export function keys(obj: T): (Extract)[] { - var result: (Extract)[] = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - result.push(key); - } - } - return result; - } - - /** - * Return the list of values from an object. - */ - export function values(obj: { [key: string]: T }): T[] { - var result: T[] = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - result.push(obj[key]); - } - } - return result; - } - - /** - * Iterate an enum values. - */ - export function iterenum(obj: T, callback: (item: number) => void) { - for (var val in obj) { - var parsed = parseInt(val, 10); - if (!isNaN(parsed)) { - callback(parsed); - } - } - } - - /** - * Collect enum values. - */ - export function enumvalues(obj: T): number[] { - let result: number[] = []; - iterenum(obj, val => result.push(val)); - return result; - } - - /** - * Create a dictionary from a list of couples - */ - export function dict(array: [string, T][]): { [index: string]: T } { - let result: { [index: string]: T } = {}; - array.forEach(([key, value]) => result[key] = value); - return result; - } - - /** - * Create a dictionnary index from a list of objects - */ - export function index(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } { - var result: { [key: string]: T } = {}; - array.forEach(obj => result[keyfunc(obj)] = obj); - return result; - } - - /** - * Add an item to the end of a list, only if not already there - */ - export function add(array: T[], item: T): boolean { - if (!contains(array, item)) { - array.push(item); - return true; - } else { - return false; - } - } - /** - * Remove an item from a list if found. Return true if changed. - */ - export function remove(array: T[], item: T): boolean { - var idx = array.indexOf(item); - if (idx >= 0) { - array.splice(idx, 1); - return true; - } else { - return false; - } - } - - /** - * Check if two standard objects are equal. - */ - export function equals(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean { - return JSON.stringify(obj1) == JSON.stringify(obj2); - } - - /** - * Call a function on any couple formed from combining two arrays. - */ - export function combicall(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void { - array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2))); - } - - /** - * Combinate two filter functions (predicates), with a boolean and. - */ - export function andfilter(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean { - return (item: T) => filt1(item) && filt2(item); - } - - /** - * Get the class name of an object. - */ - export function classname(obj: Object): string { - return (obj.constructor).name; - } - - /** - * Get the lowest item of an array, using a mapping function. - */ - export function lowest(array: T[], rating: (item: T) => number): T { - var rated = array.map((item: T): [T, number] => [item, rating(item)]); - rated.sort((a, b) => cmp(a[1], b[1])); - return rated[0][0]; - } - - /** - * Return a function bound to an object. - * - * This is useful to pass the bound function as callback directly. - */ - export function bound(obj: T, func: K): T[K] { - let attr = obj[func]; - if (attr instanceof Function) { - return attr.bind(obj); - } else { - return (() => attr); - } - } - - /** - * Return a 0.0-1.0 factor of progress between two limits. - */ - export function progress(value: number, min: number, max: number) { - var result = (value - min) / (max - min); - return clamp(result, 0.0, 1.0); - } - - /** - * Copy all fields of an object in another (shallow copy) - */ - export function copyfields(src: Partial, dest: Partial) { - for (let key in src) { - if (src.hasOwnProperty(key)) { - dest[key] = src[key]; - } - } - } - - /** - * Copy an object (only a shallow copy of immediate properties) - */ - export function copy(object: T): T { - let objectCopy = Object.create(object.constructor.prototype); - copyfields(object, objectCopy); - return objectCopy; - } - - /** - * Merge an object into another - */ - export function merge(base: T, incoming: Partial): T { - let result = copy(base); - copyfields(incoming, result); - return result; - } - - export const STOP_CRAWLING = {}; - - /** - * Recursively crawl through an object, yielding any defined value found along the way - * - * If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback - * - * *memo* is used to prevent circular references to be traversed - */ - export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) { - if (obj instanceof Object && !Array.isArray(obj)) { - if (memo.indexOf(obj) >= 0) { - return obj; - } else { - memo.push(obj); - } - } - - if (obj !== undefined && obj !== null && typeof obj != "function") { - let result = callback(obj); - - if (result === STOP_CRAWLING) { - return; - } - - if (Array.isArray(obj)) { - let subresult = obj.map(value => crawl(value, callback, replace, memo)); - if (replace) { - subresult.forEach((value, index) => { - obj[index] = value; - }); - } - } else if (obj instanceof Object) { - let subresult: any = {}; - iteritems(obj, (key, value) => { - subresult[key] = crawl(value, callback, replace, memo); - }); - if (replace) { - copyfields(subresult, obj); - } - } - - return result; - } else { - return obj; - } - } - - /** - * Return the minimal value of an array - */ - export function min(array: T[]): T { - return array.reduce((a, b) => a < b ? a : b); - } - - /** - * Return the maximal value of an array - */ - export function max(array: T[]): T { - return array.reduce((a, b) => a > b ? a : b); - } - - /** - * Return the sum of an array - */ - export function sum(array: number[]): number { - return array.reduce((a, b) => a + b, 0); - } - - /** - * Return the average of an array - */ - export function avg(array: number[]): number { - return sum(array) / array.length; - } - - /** - * Return value, with the same sign as base - */ - export function samesign(value: number, base: number): number { - return Math.abs(value) * (base < 0 ? -1 : 1); - } - - /** - * Return a copy of the array, sorted by a cmp function (equivalent of javascript sort) - */ - export function sorted(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] { - return acopy(array).sort(cmpfunc); - } - - /** - * Return a copy of the array, sorted by the result of a function applied to each item - */ - export function sortedBy(array: T1[], func: (val: T1) => T2, reverse = false): T1[] { - return sorted(array, (a, b) => cmp(func(a), func(b), reverse)); - } - - /** - * Return the minimum of an array transformed by a function - */ - export function minBy(array: T1[], func: (val: T1) => T2): T1 { - return array.reduce((a, b) => func(a) < func(b) ? a : b); - } - - /** - * Return the maximum of an array transformed by a function - */ - export function maxBy(array: T1[], func: (val: T1) => T2): T1 { - return array.reduce((a, b) => func(a) > func(b) ? a : b); - } - - /** - * Return a copy of an array, containing each value only once - */ - export function unique(array: T[]): T[] { - return array.filter((value, index, self) => self.indexOf(value) === index); - } - - /** - * Return the union of two arrays (items in either array) - */ - export function union(array1: T[], array2: T[]): T[] { - return array1.concat(difference(array2, array1)); - } - - /** - * Return the difference between two arrays (items in the first, but not in the second) - */ - export function difference(array1: T[], array2: T[]): T[] { - return array1.filter(value => !contains(array2, value)); - } - - /** - * Return the intersection of two arrays (items in both arrays) - */ - export function intersection(array1: T[], array2: T[]): T[] { - return array1.filter(value => contains(array2, value)); - } - - /** - * Return the disjunctive union of two arrays (items not in both arrays) - */ - export function disjunctunion(array1: T[], array2: T[]): T[] { - return difference(union(array1, array2), intersection(array1, array2)); - } +export function nop(): void { +} + +/** + * Identity function (returns the sole argument untouched) + */ +export function identity(input: T): T { + return input; +} + +/** + * Check a value for a boolean equivalent + */ +export function bool(value: T | null | undefined): value is T; +export function bool(value: any): boolean { + if (!value) { + return false; + } else if (typeof value == "object") { + return Object.keys(value).length > 0; + } else { + return true; + } +} + +/** + * Return a default value if the given one is undefined + */ +export function coalesce(value: string | null | undefined, fallback: string): string; +export function coalesce(value: number | null | undefined, fallback: number): number; +export function coalesce(value: T | null | undefined, fallback: T): T { + if (typeof value == "undefined" || value === null) { + return fallback; + } else { + return value; + } +} + +/** + * Check for an object being an instance of a type, an returns casted version + * + * Throws an error on failure to cast + */ +export function as(classref: { new(...args: any[]): T }, obj: any): T { + if (obj instanceof classref) { + return obj; + } else { + console.error("Bad cast", obj, classref); + throw new Error("Bad cast"); + } +} + +/** + * Apply a functor on the result of another function + */ +export function fmap(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V { + // TODO variadic typing, as soon as supported by typescript + return (...args) => g(f(...args)); +} + +/** + * Apply a default value to nulls or undefineds returned by a function + */ +export function nnf(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T { + return fmap(val => val === null ? fallback : val, f); +} + +/** + * Check if a value if null, throwing an exception if its the case + */ +export function nn(value: T | null | undefined): T { + if (value === null) { + throw new Error("Null value"); + } else if (typeof value == "undefined") { + throw new Error("Undefined value"); + } else { + return value; + } +} + +/** + * Remove null values from an array + */ +export function nna(array: (T | null)[]): T[] { + return array.filter(item => item !== null); +} + +/** + * Compare operator, that can be used in sort() calls. + */ +export function cmp(a: any, b: any, reverse = false): number { + if (a > b) { + return reverse ? -1 : 1; + } else if (a < b) { + return reverse ? 1 : -1; + } else { + return 0; + } +} + +/** + * Clamp a value in a range. + */ +export function clamp(value: T, min: T, max: T): T { + if (value < min) { + return min; + } else if (value > max) { + return max; + } else { + return value; + } +} + +/** + * Perform a linear interpolation between two values (factor is between 0 and 1). + */ +export function lerp(factor: number, min: number, max: number): number { + return min + (max - min) * factor; +} + +/** + * Make a deep copy of any object. + * + * Serializer is used for this, and needs a namespace to work. + * + * Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness. + */ +export function duplicate(obj: T, namespace: { [name: string]: (...args: any) => any } = {}): T { + let serializer = new Serializer(namespace); + let serialized = serializer.serialize({ dat: obj }); + return serializer.unserialize(serialized).dat; +} + +/** + * Make a shallow copy of an array. + */ +export function acopy(input: T[]): T[] { + return input.slice(); +} + +/** + * Call a function for each member of an array, sorted by a key. + */ +export function itersorted(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void { + var array = acopy(input); + array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2))); + array.forEach(callback); +} + +/** + * Capitalize the first letter of an input string. + */ +export function capitalize(input: string): string { + return input.charAt(0).toLocaleUpperCase() + input.slice(1); +}; + +/** + * Check if an array contains an item. + */ +export function contains(array: T[], item: T): boolean { + return array.indexOf(item) >= 0; +} + +/** + * Produce an n-sized array, with integers counting from 0 + */ +export function range(n: number): number[] { + var result: number[] = []; + for (var i = 0; i < n; i++) { + result.push(i); + } + return result; +} + +/** + * Produce an array of couples, build from the common length of two arrays + */ +export function zip(array1: T1[], array2: T2[]): [T1, T2][] { + var result: [T1, T2][] = []; + var n = (array1.length > array2.length) ? array2.length : array1.length; + for (var i = 0; i < n; i++) { + result.push([array1[i], array2[i]]); + } + return result; +} + +/** + * Produce two arrays, build from an array of couples + */ +export function unzip(array: [T1, T2][]): [T1[], T2[]] { + return [array.map(x => x[0]), array.map(x => x[1])]; +} + +/** + * Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it + */ +export function binpartition(array: T[], predicate: (item: T) => boolean): [T[], T[]] { + let pass: T[] = []; + let fail: T[] = []; + array.forEach(item => (predicate(item) ? pass : fail).push(item)); + return [pass, fail]; +} + +/** + * Yields the neighbors tuple list + */ +export function neighbors(array: T[], wrap = false): [T, T][] { + var result: [T, T][] = []; + if (array.length > 0) { + var previous = array[0]; + for (var i = 1; i < array.length; i++) { + result.push([previous, array[i]]); + previous = array[i]; + } + if (wrap) { + result.push([previous, array[0]]); + } + return result; + } else { + return []; + } +} + +/** + * Type filter, to return a list of instances of a given type + */ +export function tfilter(array: any[], filter: (item: any) => item is T): T[] { + return array.filter(filter); +} + +/** + * Class filter, to return a list of instances of a given type + */ +export function cfilter(array: any[], classref: { new(...args: any[]): T }): T[] { + return array.filter((item): item is T => item instanceof classref); +} + +/** + * Flatten a list of lists + */ +export function flatten(array: T[][]): T[] { + return array.reduce((a, b) => a.concat(b), []); +} + +/** + * Count each element in an array + */ +export function counter(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] { + var result: [T, number][] = []; + array.forEach(item => { + var found = first(result, iter => equals(iter[0], item)); + if (found) { + found[1]++; + } else { + result.push([item, 1]); + } + }); + return result; +} + +/** + * Return the first element of the array that matches the predicate, null if not found + */ +export function first(array: T[], predicate: (item: T) => boolean): T | null { + for (var i = 0; i < array.length; i++) { + if (predicate(array[i])) { + return array[i]; + } + } + return null; +} + +/** + * Return whether if any element in the array matches the predicate + */ +export function any(array: T[], predicate: (item: T) => boolean): boolean { + return first(array, predicate) != null; +} + +/** + * Return an iterator over an array + * + * An iterator is a function yielding the next value each time, until the end of array where it yields null. + * + * For more powerful iterators, see Iterators + */ +export function iterator(array: T[]): () => T | null { + let i = 0; + return () => (i < array.length) ? array[i++] : null; +} + +/** + * Iterate a list of (key, value) in an object. + */ +export function iteritems(obj: { [key: string]: T }, func: (key: string, value: T) => void) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + func(key, obj[key]); + } + } +} + +/** + * Transform an dictionary object to a list of couples (key, value). + */ +export function items(obj: { [key: string]: T }): [string, T][] { + let result: [string, T][] = []; + iteritems(obj, (key, value) => result.push([key, value])); + return result; +} + +/** + * Return the list of keys from an object. + */ +export function keys(obj: T): (Extract)[] { + var result: (Extract)[] = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + result.push(key); + } + } + return result; +} + +/** + * Return the list of values from an object. + */ +export function values(obj: { [key: string]: T }): T[] { + var result: T[] = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + result.push(obj[key]); + } + } + return result; +} + +/** + * Iterate an enum values. + */ +export function iterenum(obj: T, callback: (item: number) => void) { + for (var val in obj) { + var parsed = parseInt(val, 10); + if (!isNaN(parsed)) { + callback(parsed); + } + } +} + +/** + * Collect enum values. + */ +export function enumvalues(obj: T): number[] { + let result: number[] = []; + iterenum(obj, val => result.push(val)); + return result; +} + +/** + * Create a dictionary from a list of couples + */ +export function dict(array: [string, T][]): { [index: string]: T } { + let result: { [index: string]: T } = {}; + array.forEach(([key, value]) => result[key] = value); + return result; +} + +/** + * Create a dictionnary index from a list of objects + */ +export function index(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } { + var result: { [key: string]: T } = {}; + array.forEach(obj => result[keyfunc(obj)] = obj); + return result; +} + +/** + * Add an item to the end of a list, only if not already there + */ +export function add(array: T[], item: T): boolean { + if (!contains(array, item)) { + array.push(item); + return true; + } else { + return false; + } +} +/** + * Remove an item from a list if found. Return true if changed. + */ +export function remove(array: T[], item: T): boolean { + var idx = array.indexOf(item); + if (idx >= 0) { + array.splice(idx, 1); + return true; + } else { + return false; + } +} + +/** + * Check if two standard objects are equal. + */ +export function equals(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean { + return JSON.stringify(obj1) == JSON.stringify(obj2); +} + +/** + * Call a function on any couple formed from combining two arrays. + */ +export function combicall(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void { + array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2))); +} + +/** + * Combinate two filter functions (predicates), with a boolean and. + */ +export function andfilter(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean { + return (item: T) => filt1(item) && filt2(item); +} + +/** + * Get the class name of an object. + */ +export function classname(obj: Object): string { + return (obj.constructor).name; +} + +/** + * Get the lowest item of an array, using a mapping function. + */ +export function lowest(array: T[], rating: (item: T) => number): T { + var rated = array.map((item: T): [T, number] => [item, rating(item)]); + rated.sort((a, b) => cmp(a[1], b[1])); + return rated[0][0]; +} + +/** + * Return a function bound to an object. + * + * This is useful to pass the bound function as callback directly. + */ +export function bound(obj: T, func: K): T[K] { + let attr = obj[func]; + if (attr instanceof Function) { + return attr.bind(obj); + } else { + return (() => attr); + } +} + +/** + * Return a 0.0-1.0 factor of progress between two limits. + */ +export function progress(value: number, min: number, max: number) { + var result = (value - min) / (max - min); + return clamp(result, 0.0, 1.0); +} + +/** + * Copy all fields of an object in another (shallow copy) + */ +export function copyfields(src: Partial, dest: Partial) { + for (let key in src) { + if (src.hasOwnProperty(key)) { + dest[key] = src[key]; + } + } +} + +/** + * Copy an object (only a shallow copy of immediate properties) + */ +export function copy(object: T): T { + let objectCopy = Object.create(object.constructor.prototype); + copyfields(object, objectCopy); + return objectCopy; +} + +/** + * Merge an object into another + */ +export function merge(base: T, incoming: Partial): T { + let result = copy(base); + copyfields(incoming, result); + return result; +} + +export const STOP_CRAWLING = {}; + +/** + * Recursively crawl through an object, yielding any defined value found along the way + * + * If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback + * + * *memo* is used to prevent circular references to be traversed + */ +export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) { + if (obj instanceof Object && !Array.isArray(obj)) { + if (memo.indexOf(obj) >= 0) { + return obj; + } else { + memo.push(obj); + } + } + + if (obj !== undefined && obj !== null && typeof obj != "function") { + let result = callback(obj); + + if (result === STOP_CRAWLING) { + return; + } + + if (Array.isArray(obj)) { + let subresult = obj.map(value => crawl(value, callback, replace, memo)); + if (replace) { + subresult.forEach((value, index) => { + obj[index] = value; + }); + } + } else if (obj instanceof Object) { + let subresult: any = {}; + iteritems(obj, (key, value) => { + subresult[key] = crawl(value, callback, replace, memo); + }); + if (replace) { + copyfields(subresult, obj); + } + } + + return result; + } else { + return obj; + } +} + +/** + * Return the minimal value of an array + */ +export function min(array: T[]): T { + return array.reduce((a, b) => a < b ? a : b); +} + +/** + * Return the maximal value of an array + */ +export function max(array: T[]): T { + return array.reduce((a, b) => a > b ? a : b); +} + +/** + * Return the sum of an array + */ +export function sum(array: number[]): number { + return array.reduce((a, b) => a + b, 0); +} + +/** + * Return the average of an array + */ +export function avg(array: number[]): number { + return sum(array) / array.length; +} + +/** + * Return value, with the same sign as base + */ +export function samesign(value: number, base: number): number { + return Math.abs(value) * (base < 0 ? -1 : 1); +} + +/** + * Return a copy of the array, sorted by a cmp function (equivalent of javascript sort) + */ +export function sorted(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] { + return acopy(array).sort(cmpfunc); +} + +/** + * Return a copy of the array, sorted by the result of a function applied to each item + */ +export function sortedBy(array: T1[], func: (val: T1) => T2, reverse = false): T1[] { + return sorted(array, (a, b) => cmp(func(a), func(b), reverse)); +} + +/** + * Return the minimum of an array transformed by a function + */ +export function minBy(array: T1[], func: (val: T1) => T2): T1 { + return array.reduce((a, b) => func(a) < func(b) ? a : b); +} + +/** + * Return the maximum of an array transformed by a function + */ +export function maxBy(array: T1[], func: (val: T1) => T2): T1 { + return array.reduce((a, b) => func(a) > func(b) ? a : b); +} + +/** + * Return a copy of an array, containing each value only once + */ +export function unique(array: T[]): T[] { + return array.filter((value, index, self) => self.indexOf(value) === index); +} + +/** + * Return the union of two arrays (items in either array) + */ +export function union(array1: T[], array2: T[]): T[] { + return array1.concat(difference(array2, array1)); +} + +/** + * Return the difference between two arrays (items in the first, but not in the second) + */ +export function difference(array1: T[], array2: T[]): T[] { + return array1.filter(value => !contains(array2, value)); +} + +/** + * Return the intersection of two arrays (items in both arrays) + */ +export function intersection(array1: T[], array2: T[]): T[] { + return array1.filter(value => contains(array2, value)); +} + +/** + * Return the disjunctive union of two arrays (items not in both arrays) + */ +export function disjunctunion(array1: T[], array2: T[]): T[] { + return difference(union(array1, array2), intersection(array1, array2)); } diff --git a/src/core/ArenaGrid.spec.ts b/src/core/ArenaGrid.spec.ts index c2cd018..027f4a0 100644 --- a/src/core/ArenaGrid.spec.ts +++ b/src/core/ArenaGrid.spec.ts @@ -1,29 +1,27 @@ -module TK.SpaceTac.Specs { - function checkLocation(check: TestContext, got: IArenaLocation, expected_x: number, expected_y: number) { - check.equals(got.x, expected_x, `x differs (${got.x},${got.y}) (${expected_x},${expected_y})`); - check.equals(got.y, expected_y, `y differs (${got.x},${got.y}) (${expected_x},${expected_y})`); - } - - testing("HexagonalArenaGrid", test => { - test.case("snaps coordinates to the nearest grid point, on a biased grid", check => { - let grid = new HexagonalArenaGrid(4, 0.75); - checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 2.1, y: 0 }), 4, 0); - checkLocation(check, grid.snap({ x: 1, y: 1 }), 0, 0); - checkLocation(check, grid.snap({ x: 1, y: 2 }), 2, 3); - checkLocation(check, grid.snap({ x: -1, y: -1 }), 0, 0); - checkLocation(check, grid.snap({ x: -2, y: -2 }), -2, -3); - checkLocation(check, grid.snap({ x: -3, y: -1 }), -4, 0); - checkLocation(check, grid.snap({ x: 6, y: -5 }), 8, -6); - }); - - test.case("snaps coordinates to the nearest grid point, on a regular grid", check => { - let grid = new HexagonalArenaGrid(10); - checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); - checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0); - checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75)); - }); - }); +function checkLocation(check: TestContext, got: IArenaLocation, expected_x: number, expected_y: number) { + check.equals(got.x, expected_x, `x differs (${got.x},${got.y}) (${expected_x},${expected_y})`); + check.equals(got.y, expected_y, `y differs (${got.x},${got.y}) (${expected_x},${expected_y})`); } + +testing("HexagonalArenaGrid", test => { + test.case("snaps coordinates to the nearest grid point, on a biased grid", check => { + let grid = new HexagonalArenaGrid(4, 0.75); + checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); + checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0); + checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0); + checkLocation(check, grid.snap({ x: 2.1, y: 0 }), 4, 0); + checkLocation(check, grid.snap({ x: 1, y: 1 }), 0, 0); + checkLocation(check, grid.snap({ x: 1, y: 2 }), 2, 3); + checkLocation(check, grid.snap({ x: -1, y: -1 }), 0, 0); + checkLocation(check, grid.snap({ x: -2, y: -2 }), -2, -3); + checkLocation(check, grid.snap({ x: -3, y: -1 }), -4, 0); + checkLocation(check, grid.snap({ x: 6, y: -5 }), 8, -6); + }); + + test.case("snaps coordinates to the nearest grid point, on a regular grid", check => { + let grid = new HexagonalArenaGrid(10); + checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); + checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0); + checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75)); + }); +}); diff --git a/src/core/ArenaGrid.ts b/src/core/ArenaGrid.ts index b9a6aef..a6991d2 100644 --- a/src/core/ArenaGrid.ts +++ b/src/core/ArenaGrid.ts @@ -1,34 +1,34 @@ -module TK.SpaceTac { - /** - * Abstract grid for the arena where the battle takes place - * - * The grid is used to snap arena coordinates for ships and targets - */ - export interface IArenaGrid { - snap(loc: IArenaLocation): IArenaLocation; - } +import { ArenaLocation, IArenaLocation } from "./ArenaLocation"; - /** - * Hexagonal unbounded arena grid - * - * This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center - */ - export class HexagonalArenaGrid implements IArenaGrid { - private yunit: number; - - constructor(private unit: number, private yfactor = Math.sqrt(0.75)) { - this.yunit = unit * yfactor; - } - - snap(loc: IArenaLocation): IArenaLocation { - let yr = Math.round(loc.y / this.yunit); - let xr: number; - if (yr % 2 == 0) { - xr = Math.round(loc.x / this.unit); - } else { - xr = Math.round((loc.x - 0.5 * this.unit) / this.unit) + 0.5; - } - return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0); - } - } +/** + * Abstract grid for the arena where the battle takes place + * + * The grid is used to snap arena coordinates for ships and targets + */ +export interface IArenaGrid { + snap(loc: IArenaLocation): IArenaLocation; +} + +/** + * Hexagonal unbounded arena grid + * + * This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center + */ +export class HexagonalArenaGrid implements IArenaGrid { + private yunit: number; + + constructor(private unit: number, private yfactor = Math.sqrt(0.75)) { + this.yunit = unit * yfactor; + } + + snap(loc: IArenaLocation): IArenaLocation { + let yr = Math.round(loc.y / this.yunit); + let xr: number; + if (yr % 2 == 0) { + xr = Math.round(loc.x / this.unit); + } else { + xr = Math.round((loc.x - 0.5 * this.unit) / this.unit) + 0.5; + } + return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0); + } } diff --git a/src/core/ArenaLocation.spec.ts b/src/core/ArenaLocation.spec.ts index b821e91..f58cd64 100644 --- a/src/core/ArenaLocation.spec.ts +++ b/src/core/ArenaLocation.spec.ts @@ -1,22 +1,20 @@ -module TK.SpaceTac.Specs { - testing("ArenaLocation", test => { - test.case("gets distance and angle between two locations", check => { - check.nears(arenaDistance({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.sqrt(2)); - check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4); - }) +testing("ArenaLocation", test => { + test.case("gets distance and angle between two locations", check => { + check.nears(arenaDistance({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.sqrt(2)); + check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4); + }) - test.case("computes an angular difference", check => { - check.equals(angularDifference(0.5, 1.5), 1.0); - check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0); - check.same(angularDifference(0.5, -0.5), -1.0); - check.nears(angularDifference(0.5, -0.3 - Math.PI * 4), -0.8); - check.nears(angularDifference(-3 * Math.PI / 4, 3 * Math.PI / 4), -Math.PI / 2); - check.nears(angularDifference(3 * Math.PI / 4, -3 * Math.PI / 4), Math.PI / 2); - }) + test.case("computes an angular difference", check => { + check.equals(angularDifference(0.5, 1.5), 1.0); + check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0); + check.same(angularDifference(0.5, -0.5), -1.0); + check.nears(angularDifference(0.5, -0.3 - Math.PI * 4), -0.8); + check.nears(angularDifference(-3 * Math.PI / 4, 3 * Math.PI / 4), -Math.PI / 2); + check.nears(angularDifference(3 * Math.PI / 4, -3 * Math.PI / 4), Math.PI / 2); + }) - test.case("converts between degrees and radians", check => { - check.nears(degrees(Math.PI / 2), 90); - check.nears(radians(45), Math.PI / 4); - }); - }); -} + test.case("converts between degrees and radians", check => { + check.nears(degrees(Math.PI / 2), 90); + check.nears(radians(45), Math.PI / 4); + }); +}); diff --git a/src/core/ArenaLocation.ts b/src/core/ArenaLocation.ts index 59eed53..1635038 100644 --- a/src/core/ArenaLocation.ts +++ b/src/core/ArenaLocation.ts @@ -1,99 +1,97 @@ -module TK.SpaceTac { - /** - * Location in the arena (coordinates only) - */ - export interface IArenaLocation { - x: number - y: number - } - export class ArenaLocation implements IArenaLocation { - x: number - y: number - - constructor(x = 0, y = 0) { - this.x = x; - this.y = y; - } - } - - /** - * Location in the arena, with a facing angle in radians - */ - export interface IArenaLocationAngle { - x: number - y: number - angle: number - } - export class ArenaLocationAngle extends ArenaLocation implements IArenaLocationAngle { - angle: number - - constructor(x = 0, y = 0, angle = 0) { - super(x, y); - this.angle = angle; - } - } - - /** - * Circle area in the arena - */ - export interface IArenaCircleArea { - x: number - y: number - radius: number - } - - export class ArenaCircleArea extends ArenaLocation implements IArenaCircleArea { - radius: number - - constructor(x = 0, y = 0, radius = 0) { - super(x, y); - this.radius = radius; - } - } - - /** - * Get the normalized angle (in radians) between two locations - */ - export function arenaAngle(loc1: IArenaLocation, loc2: IArenaLocation): number { - return Math.atan2(loc2.y - loc1.y, loc2.x - loc1.x); - } - - /** - * Get the "angular difference" between two angles in radians, in ]-pi,pi] range. - */ - export function angularDifference(angle1: number, angle2: number): number { - let diff = angle2 - angle1; - return diff - Math.PI * 2 * Math.floor((diff + Math.PI) / (Math.PI * 2)); - } - - /** - * Get the normalized distance between two locations - */ - export function arenaDistance(loc1: IArenaLocation, loc2: IArenaLocation): number { - let dx = loc2.x - loc1.x; - let dy = loc2.y - loc1.y; - return Math.sqrt(dx * dx + dy * dy); - } - - /** - * Check if a location is inside an area - */ - export function arenaInside(loc1: IArenaLocation, loc2: IArenaCircleArea, border_inclusive = true): boolean { - let dist = arenaDistance(loc1, loc2); - return border_inclusive ? (dist <= loc2.radius) : (dist < loc2.radius); - } - - /** - * Convert radians angle to degrees - */ - export function degrees(angle: number): number { - return angle * 180 / Math.PI; - } - - /** - * Convert degrees angle to radians - */ - export function radians(angle: number): number { - return angle * Math.PI / 180; - } +/** + * Location in the arena (coordinates only) + */ +export interface IArenaLocation { + x: number + y: number +} +export class ArenaLocation implements IArenaLocation { + x: number + y: number + + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } +} + +/** + * Location in the arena, with a facing angle in radians + */ +export interface IArenaLocationAngle { + x: number + y: number + angle: number +} +export class ArenaLocationAngle extends ArenaLocation implements IArenaLocationAngle { + angle: number + + constructor(x = 0, y = 0, angle = 0) { + super(x, y); + this.angle = angle; + } +} + +/** + * Circle area in the arena + */ +export interface IArenaCircleArea { + x: number + y: number + radius: number +} + +export class ArenaCircleArea extends ArenaLocation implements IArenaCircleArea { + radius: number + + constructor(x = 0, y = 0, radius = 0) { + super(x, y); + this.radius = radius; + } +} + +/** + * Get the normalized angle (in radians) between two locations + */ +export function arenaAngle(loc1: IArenaLocation, loc2: IArenaLocation): number { + return Math.atan2(loc2.y - loc1.y, loc2.x - loc1.x); +} + +/** + * Get the "angular difference" between two angles in radians, in ]-pi,pi] range. + */ +export function angularDifference(angle1: number, angle2: number): number { + let diff = angle2 - angle1; + return diff - Math.PI * 2 * Math.floor((diff + Math.PI) / (Math.PI * 2)); +} + +/** + * Get the normalized distance between two locations + */ +export function arenaDistance(loc1: IArenaLocation, loc2: IArenaLocation): number { + let dx = loc2.x - loc1.x; + let dy = loc2.y - loc1.y; + return Math.sqrt(dx * dx + dy * dy); +} + +/** + * Check if a location is inside an area + */ +export function arenaInside(loc1: IArenaLocation, loc2: IArenaCircleArea, border_inclusive = true): boolean { + let dist = arenaDistance(loc1, loc2); + return border_inclusive ? (dist <= loc2.radius) : (dist < loc2.radius); +} + +/** + * Convert radians angle to degrees + */ +export function degrees(angle: number): number { + return angle * 180 / Math.PI; +} + +/** + * Convert degrees angle to radians + */ +export function radians(angle: number): number { + return angle * Math.PI / 180; } diff --git a/src/core/Battle.spec.ts b/src/core/Battle.spec.ts index 0bf08c9..d6c549d 100644 --- a/src/core/Battle.spec.ts +++ b/src/core/Battle.spec.ts @@ -1,381 +1,379 @@ -module TK.SpaceTac { - testing("Battle", test => { - test.case("defines play order by initiative throws", check => { - var fleet1 = new Fleet(); - var fleet2 = new Fleet(); - - var ship1 = new Ship(fleet1, "F1S1"); - TestTools.setAttribute(ship1, "initiative", 2); - var ship2 = new Ship(fleet1, "F1S2"); - TestTools.setAttribute(ship2, "initiative", 4); - var ship3 = new Ship(fleet1, "F1S3"); - TestTools.setAttribute(ship3, "initiative", 1); - var ship4 = new Ship(fleet2, "F2S1"); - TestTools.setAttribute(ship4, "initiative", 8); - var ship5 = new Ship(fleet2, "F2S2"); - TestTools.setAttribute(ship5, "initiative", 2); - - var battle = new Battle(fleet1, fleet2); - check.equals(battle.play_order.length, 0); - - var gen = new SkewedRandomGenerator([1.0, 0.1, 1.0, 0.2, 0.6]); - battle.throwInitiative(gen); - - check.equals(battle.play_order.length, 5); - check.equals(battle.play_order, [ship1, ship4, ship5, ship3, ship2]); - }); - - test.case("places ships on lines, facing the arena center", check => { - var fleet1 = new Fleet(); - var fleet2 = new Fleet(); - - var ship1 = new Ship(fleet1, "F1S1"); - var ship2 = new Ship(fleet1, "F1S2"); - var ship3 = new Ship(fleet1, "F1S3"); - var ship4 = new Ship(fleet2, "F2S1"); - var ship5 = new Ship(fleet2, "F2S2"); - - var battle = new Battle(fleet1, fleet2, 1000, 500); - battle.placeShips(); - - check.nears(ship1.arena_x, 250); - check.nears(ship1.arena_y, 150); - check.nears(ship1.arena_angle, 0); - - check.nears(ship2.arena_x, 250); - check.nears(ship2.arena_y, 250); - check.nears(ship2.arena_angle, 0); - - check.nears(ship3.arena_x, 250); - check.nears(ship3.arena_y, 350); - check.nears(ship3.arena_angle, 0); - - check.nears(ship4.arena_x, 750); - check.nears(ship4.arena_y, 300); - check.nears(ship4.arena_angle, Math.PI); - - check.nears(ship5.arena_x, 750); - check.nears(ship5.arena_y, 200); - check.nears(ship5.arena_angle, Math.PI); - }); - - test.case("advances to next ship in play order", check => { - var fleet1 = new Fleet(); - var fleet2 = new Fleet(); - - var ship1 = new Ship(fleet1, "ship1"); - var ship2 = new Ship(fleet1, "ship2"); - var ship3 = new Ship(fleet2, "ship3"); - - var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); - - // Check empty play_order case - check.equals(battle.playing_ship, null); - battle.advanceToNextShip(); - check.equals(battle.playing_ship, null); - - // Force play order - iforeach(battle.iships(), ship => TestTools.setAttribute(ship, "initiative", 1)); - var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]); - battle.throwInitiative(gen); - check.equals(battle.playing_ship, null); - - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship2); - - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship1); - - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship3); - - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship2); - - // A dead ship is skipped - ship1.setDead(); - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship3); - - // Playing ship dies - ship3.setDead(); - battle.advanceToNextShip(); - check.same(battle.playing_ship, ship2); - }); - - test.case("handles the suicide case (playing ship dies because of its action)", check => { - let battle = TestTools.createBattle(3, 1); - let [ship1, ship2, ship3, ship4] = battle.play_order; - ship1.setArenaPosition(0, 0); - ship2.setArenaPosition(0, 0); - ship3.setArenaPosition(1000, 1000); - ship4.setArenaPosition(1000, 1000); - let weapon = TestTools.addWeapon(ship1, 8000, 0, 50, 100); - - check.in("initially", check => { - check.same(battle.playing_ship, ship1, "playing ship"); - check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships"); - }); - - let result = battle.applyOneAction(weapon.id, Target.newFromLocation(0, 0)); - check.equals(result, true, "action applied successfully"); - check.in("after weapon", check => { - check.same(battle.playing_ship, ship3, "playing ship"); - check.equals(battle.ships.list().filter(ship => ship.alive), [ship3, ship4], "alive ships"); - }); - }); - - test.case("detects victory condition and logs a final EndBattleEvent", check => { - var fleet1 = new Fleet(); - var fleet2 = new Fleet(); - - var ship1 = new Ship(fleet1, "F1S1"); - var ship2 = new Ship(fleet1, "F1S2"); - let ship3 = new Ship(fleet2, "F2S1"); - - var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); - battle.start(); - battle.play_order = [ship3, ship2, ship1]; - check.equals(battle.ended, false); - - ship1.setDead(); - ship2.setDead(); - battle.advanceToNextShip(); - - check.equals(battle.ended, true); - let diff = battle.log.get(battle.log.count() - 1); - if (diff instanceof EndBattleDiff) { - check.notequals(diff.outcome.winner, null); - check.same(diff.outcome.winner, fleet2.id); - } else { - check.fail("Not an EndBattleDiff"); - } - }); - - test.case("handles a draw in end battle", check => { - var fleet1 = new Fleet(); - var fleet2 = new Fleet(); - - var ship1 = new Ship(fleet1, "F1S1"); - var ship2 = new Ship(fleet1, "F1S2"); - var ship3 = new Ship(fleet2, "F2S1"); - - var battle = new Battle(fleet1, fleet2); - - battle.start(); - check.equals(battle.ended, false); - - ship1.setDead(); - ship2.setDead(); - ship3.setDead(); - - battle.log.clear(); - check.equals(battle.ended, false); - battle.performChecks(); - - check.equals(battle.ended, true); - check.equals(battle.log.count(), 1); - let diff = battle.log.get(0); - if (diff instanceof EndBattleDiff) { - check.equals(diff.outcome.winner, null); - } else { - check.fail("Not an EndBattleDiff"); - } - }); - - test.case("collects ships present in a circle", check => { - var fleet1 = new Fleet(); - var ship1 = new Ship(fleet1, "F1S1"); - ship1.setArenaPosition(0, 0); - var ship2 = new Ship(fleet1, "F1S2"); - ship2.setArenaPosition(5, 8); - var ship3 = new Ship(fleet1, "F1S3"); - ship3.setArenaPosition(6.5, 9.5); - var ship4 = new Ship(fleet1, "F1S4"); - ship4.setArenaPosition(12, 12); - - var battle = new Battle(fleet1); - battle.throwInitiative(new SkewedRandomGenerator([5, 4, 3, 2])); - - var result = battle.collectShipsInCircle(Target.newFromLocation(5, 8), 3); - check.equals(result, [ship2, ship3]); - }); - - test.case("adds and remove drones", check => { - let battle = new Battle(); - let ship = new Ship(); - let drone = new Drone(ship); - check.equals(battle.drones.count(), 0); - - battle.addDrone(drone); - check.equals(battle.drones.count(), 1); - check.same(battle.drones.get(drone.id), drone); - - battle.addDrone(drone); - check.equals(battle.drones.count(), 1); - - battle.removeDrone(drone); - check.equals(battle.drones.count(), 0); - - battle.removeDrone(drone); - check.equals(battle.drones.count(), 0); - }); - - test.case("checks if a player is able to play", check => { - let battle = new Battle(); - let player = new Player(); - - check.equals(battle.canPlay(player), false); - - let ship = new Ship(); - TestTools.setShipPlaying(battle, ship); - - check.equals(battle.canPlay(player), false); - - ship.fleet.setPlayer(player); - - check.equals(battle.canPlay(player), true); - }); - - test.case("gets the number of turns before a specific ship plays", check => { - let battle = TestTools.createBattle(2, 1); - - check.in("initial", check => { - check.same(battle.playing_ship, battle.play_order[0], "first ship playing"); - check.equals(battle.getPlayOrder(battle.play_order[0]), 0); - check.equals(battle.getPlayOrder(battle.play_order[1]), 1); - check.equals(battle.getPlayOrder(battle.play_order[2]), 2); - }); - - battle.advanceToNextShip(); - - check.in("1 step", check => { - check.same(battle.playing_ship, battle.play_order[1], "second ship playing"); - check.equals(battle.getPlayOrder(battle.play_order[0]), 2); - check.equals(battle.getPlayOrder(battle.play_order[1]), 0); - check.equals(battle.getPlayOrder(battle.play_order[2]), 1); - }); - - battle.advanceToNextShip(); - - check.in("2 steps", check => { - check.same(battle.playing_ship, battle.play_order[2], "third ship playing"); - check.equals(battle.getPlayOrder(battle.play_order[0]), 1); - check.equals(battle.getPlayOrder(battle.play_order[1]), 2); - check.equals(battle.getPlayOrder(battle.play_order[2]), 0); - }); - - battle.advanceToNextShip(); - - check.in("3 steps", check => { - check.same(battle.playing_ship, battle.play_order[0], "first ship playing"); - check.equals(battle.getPlayOrder(battle.play_order[0]), 0); - check.equals(battle.getPlayOrder(battle.play_order[1]), 1); - check.equals(battle.getPlayOrder(battle.play_order[2]), 2); - }); - }); - - test.case("lists area effects", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let peer = battle.fleets[1].addShip(); - peer.setArenaPosition(100, 50); - - check.equals(battle.getAreaEffects(peer), [], "initial"); - - let drone1 = new Drone(ship); - drone1.x = 120; - drone1.y = 60; - drone1.radius = 40; - drone1.effects = [new DamageEffect(12)]; - battle.addDrone(drone1); - let drone2 = new Drone(ship); - drone2.x = 130; - drone2.y = 70; - drone2.radius = 20; - drone2.effects = [new DamageEffect(14)]; - battle.addDrone(drone2); - - check.equals(battle.getAreaEffects(peer), [[drone1, drone1.effects[0]]], "drone effects"); - - let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 1)] }); - ship.actions.addCustom(eq1); - ship.actions.toggle(eq1, true); - let eq2 = new ToggleAction("eq2", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 2)] }); - ship.actions.addCustom(eq2); - ship.actions.toggle(eq2, false); - let eq3 = new ToggleAction("eq3", { power: 0, radius: 100, effects: [new AttributeEffect("initiative", 3)] }); - ship.actions.addCustom(eq3); - ship.actions.toggle(eq3, true); - - check.equals(battle.getAreaEffects(peer), [ - [drone1, drone1.effects[0]], - [ship, eq1.effects[0]], - ], "drone and toggle effects"); - }); - - test.case("is serializable", check => { - let battle = Battle.newQuickRandom(); - battle.ai_playing = true; - - let serializer = new Serializer(TK.SpaceTac); - let data = serializer.serialize(battle); - - let loaded = serializer.unserialize(data); - - check.equals(loaded.ai_playing, false, "ai playing is reset"); - battle.ai_playing = false; - check.equals(loaded, battle, "unserialized == initial"); - - let session = new GameSession(); - session.startNewGame(); - session.start_location.setupEncounter(); - session.start_location.enterLocation(session.player.fleet); - let battle1 = nn(session.getBattle()); - let data1 = serializer.serialize(battle1); - - let ratio = data.length / data1.length; - check.greaterorequal(ratio, 1.2, `quick battle serialized size (${data.length}) should be larger than campaign's (${data1.length})`); - }); - - test.case("can revert the last action", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - ship.setValue("hull", 13); - battle.log.clear(); - battle.log.add(new ShipValueDiff(ship, "hull", 4)); - battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); - battle.log.add(new ShipValueDiff(ship, "hull", 7)); - battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); - battle.log.add(new ShipValueDiff(ship, "hull", 2)); - - check.in("initial state", check => { - check.equals(ship.getValue("hull"), 13, "hull=13"); - check.equals(battle.log.count(), 5, "log count=5"); - }); - - battle.revertOneAction(); - - check.in("revert 1 action", check => { - check.equals(ship.getValue("hull"), 11, "hull=11"); - check.equals(battle.log.count(), 3, "log count=3"); - }); - - battle.revertOneAction(); - - check.in("revert 2 actions", check => { - check.equals(ship.getValue("hull"), 4, "hull=4"); - check.equals(battle.log.count(), 1, "log count=1"); - }); - - battle.revertOneAction(); - - check.in("revert 3 actions", check => { - check.equals(ship.getValue("hull"), 0, "hull=0"); - check.equals(battle.log.count(), 0, "log count=0"); - }); - }) +testing("Battle", test => { + test.case("defines play order by initiative throws", check => { + var fleet1 = new Fleet(); + var fleet2 = new Fleet(); + + var ship1 = new Ship(fleet1, "F1S1"); + TestTools.setAttribute(ship1, "initiative", 2); + var ship2 = new Ship(fleet1, "F1S2"); + TestTools.setAttribute(ship2, "initiative", 4); + var ship3 = new Ship(fleet1, "F1S3"); + TestTools.setAttribute(ship3, "initiative", 1); + var ship4 = new Ship(fleet2, "F2S1"); + TestTools.setAttribute(ship4, "initiative", 8); + var ship5 = new Ship(fleet2, "F2S2"); + TestTools.setAttribute(ship5, "initiative", 2); + + var battle = new Battle(fleet1, fleet2); + check.equals(battle.play_order.length, 0); + + var gen = new SkewedRandomGenerator([1.0, 0.1, 1.0, 0.2, 0.6]); + battle.throwInitiative(gen); + + check.equals(battle.play_order.length, 5); + check.equals(battle.play_order, [ship1, ship4, ship5, ship3, ship2]); + }); + + test.case("places ships on lines, facing the arena center", check => { + var fleet1 = new Fleet(); + var fleet2 = new Fleet(); + + var ship1 = new Ship(fleet1, "F1S1"); + var ship2 = new Ship(fleet1, "F1S2"); + var ship3 = new Ship(fleet1, "F1S3"); + var ship4 = new Ship(fleet2, "F2S1"); + var ship5 = new Ship(fleet2, "F2S2"); + + var battle = new Battle(fleet1, fleet2, 1000, 500); + battle.placeShips(); + + check.nears(ship1.arena_x, 250); + check.nears(ship1.arena_y, 150); + check.nears(ship1.arena_angle, 0); + + check.nears(ship2.arena_x, 250); + check.nears(ship2.arena_y, 250); + check.nears(ship2.arena_angle, 0); + + check.nears(ship3.arena_x, 250); + check.nears(ship3.arena_y, 350); + check.nears(ship3.arena_angle, 0); + + check.nears(ship4.arena_x, 750); + check.nears(ship4.arena_y, 300); + check.nears(ship4.arena_angle, Math.PI); + + check.nears(ship5.arena_x, 750); + check.nears(ship5.arena_y, 200); + check.nears(ship5.arena_angle, Math.PI); + }); + + test.case("advances to next ship in play order", check => { + var fleet1 = new Fleet(); + var fleet2 = new Fleet(); + + var ship1 = new Ship(fleet1, "ship1"); + var ship2 = new Ship(fleet1, "ship2"); + var ship3 = new Ship(fleet2, "ship3"); + + var battle = new Battle(fleet1, fleet2); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); + + // Check empty play_order case + check.equals(battle.playing_ship, null); + battle.advanceToNextShip(); + check.equals(battle.playing_ship, null); + + // Force play order + iforeach(battle.iships(), ship => TestTools.setAttribute(ship, "initiative", 1)); + var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]); + battle.throwInitiative(gen); + check.equals(battle.playing_ship, null); + + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship2); + + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship1); + + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship3); + + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship2); + + // A dead ship is skipped + ship1.setDead(); + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship3); + + // Playing ship dies + ship3.setDead(); + battle.advanceToNextShip(); + check.same(battle.playing_ship, ship2); + }); + + test.case("handles the suicide case (playing ship dies because of its action)", check => { + let battle = TestTools.createBattle(3, 1); + let [ship1, ship2, ship3, ship4] = battle.play_order; + ship1.setArenaPosition(0, 0); + ship2.setArenaPosition(0, 0); + ship3.setArenaPosition(1000, 1000); + ship4.setArenaPosition(1000, 1000); + let weapon = TestTools.addWeapon(ship1, 8000, 0, 50, 100); + + check.in("initially", check => { + check.same(battle.playing_ship, ship1, "playing ship"); + check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships"); }); -} + + let result = battle.applyOneAction(weapon.id, Target.newFromLocation(0, 0)); + check.equals(result, true, "action applied successfully"); + check.in("after weapon", check => { + check.same(battle.playing_ship, ship3, "playing ship"); + check.equals(battle.ships.list().filter(ship => ship.alive), [ship3, ship4], "alive ships"); + }); + }); + + test.case("detects victory condition and logs a final EndBattleEvent", check => { + var fleet1 = new Fleet(); + var fleet2 = new Fleet(); + + var ship1 = new Ship(fleet1, "F1S1"); + var ship2 = new Ship(fleet1, "F1S2"); + let ship3 = new Ship(fleet2, "F2S1"); + + var battle = new Battle(fleet1, fleet2); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); + battle.start(); + battle.play_order = [ship3, ship2, ship1]; + check.equals(battle.ended, false); + + ship1.setDead(); + ship2.setDead(); + battle.advanceToNextShip(); + + check.equals(battle.ended, true); + let diff = battle.log.get(battle.log.count() - 1); + if (diff instanceof EndBattleDiff) { + check.notequals(diff.outcome.winner, null); + check.same(diff.outcome.winner, fleet2.id); + } else { + check.fail("Not an EndBattleDiff"); + } + }); + + test.case("handles a draw in end battle", check => { + var fleet1 = new Fleet(); + var fleet2 = new Fleet(); + + var ship1 = new Ship(fleet1, "F1S1"); + var ship2 = new Ship(fleet1, "F1S2"); + var ship3 = new Ship(fleet2, "F2S1"); + + var battle = new Battle(fleet1, fleet2); + + battle.start(); + check.equals(battle.ended, false); + + ship1.setDead(); + ship2.setDead(); + ship3.setDead(); + + battle.log.clear(); + check.equals(battle.ended, false); + battle.performChecks(); + + check.equals(battle.ended, true); + check.equals(battle.log.count(), 1); + let diff = battle.log.get(0); + if (diff instanceof EndBattleDiff) { + check.equals(diff.outcome.winner, null); + } else { + check.fail("Not an EndBattleDiff"); + } + }); + + test.case("collects ships present in a circle", check => { + var fleet1 = new Fleet(); + var ship1 = new Ship(fleet1, "F1S1"); + ship1.setArenaPosition(0, 0); + var ship2 = new Ship(fleet1, "F1S2"); + ship2.setArenaPosition(5, 8); + var ship3 = new Ship(fleet1, "F1S3"); + ship3.setArenaPosition(6.5, 9.5); + var ship4 = new Ship(fleet1, "F1S4"); + ship4.setArenaPosition(12, 12); + + var battle = new Battle(fleet1); + battle.throwInitiative(new SkewedRandomGenerator([5, 4, 3, 2])); + + var result = battle.collectShipsInCircle(Target.newFromLocation(5, 8), 3); + check.equals(result, [ship2, ship3]); + }); + + test.case("adds and remove drones", check => { + let battle = new Battle(); + let ship = new Ship(); + let drone = new Drone(ship); + check.equals(battle.drones.count(), 0); + + battle.addDrone(drone); + check.equals(battle.drones.count(), 1); + check.same(battle.drones.get(drone.id), drone); + + battle.addDrone(drone); + check.equals(battle.drones.count(), 1); + + battle.removeDrone(drone); + check.equals(battle.drones.count(), 0); + + battle.removeDrone(drone); + check.equals(battle.drones.count(), 0); + }); + + test.case("checks if a player is able to play", check => { + let battle = new Battle(); + let player = new Player(); + + check.equals(battle.canPlay(player), false); + + let ship = new Ship(); + TestTools.setShipPlaying(battle, ship); + + check.equals(battle.canPlay(player), false); + + ship.fleet.setPlayer(player); + + check.equals(battle.canPlay(player), true); + }); + + test.case("gets the number of turns before a specific ship plays", check => { + let battle = TestTools.createBattle(2, 1); + + check.in("initial", check => { + check.same(battle.playing_ship, battle.play_order[0], "first ship playing"); + check.equals(battle.getPlayOrder(battle.play_order[0]), 0); + check.equals(battle.getPlayOrder(battle.play_order[1]), 1); + check.equals(battle.getPlayOrder(battle.play_order[2]), 2); + }); + + battle.advanceToNextShip(); + + check.in("1 step", check => { + check.same(battle.playing_ship, battle.play_order[1], "second ship playing"); + check.equals(battle.getPlayOrder(battle.play_order[0]), 2); + check.equals(battle.getPlayOrder(battle.play_order[1]), 0); + check.equals(battle.getPlayOrder(battle.play_order[2]), 1); + }); + + battle.advanceToNextShip(); + + check.in("2 steps", check => { + check.same(battle.playing_ship, battle.play_order[2], "third ship playing"); + check.equals(battle.getPlayOrder(battle.play_order[0]), 1); + check.equals(battle.getPlayOrder(battle.play_order[1]), 2); + check.equals(battle.getPlayOrder(battle.play_order[2]), 0); + }); + + battle.advanceToNextShip(); + + check.in("3 steps", check => { + check.same(battle.playing_ship, battle.play_order[0], "first ship playing"); + check.equals(battle.getPlayOrder(battle.play_order[0]), 0); + check.equals(battle.getPlayOrder(battle.play_order[1]), 1); + check.equals(battle.getPlayOrder(battle.play_order[2]), 2); + }); + }); + + test.case("lists area effects", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let peer = battle.fleets[1].addShip(); + peer.setArenaPosition(100, 50); + + check.equals(battle.getAreaEffects(peer), [], "initial"); + + let drone1 = new Drone(ship); + drone1.x = 120; + drone1.y = 60; + drone1.radius = 40; + drone1.effects = [new DamageEffect(12)]; + battle.addDrone(drone1); + let drone2 = new Drone(ship); + drone2.x = 130; + drone2.y = 70; + drone2.radius = 20; + drone2.effects = [new DamageEffect(14)]; + battle.addDrone(drone2); + + check.equals(battle.getAreaEffects(peer), [[drone1, drone1.effects[0]]], "drone effects"); + + let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 1)] }); + ship.actions.addCustom(eq1); + ship.actions.toggle(eq1, true); + let eq2 = new ToggleAction("eq2", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 2)] }); + ship.actions.addCustom(eq2); + ship.actions.toggle(eq2, false); + let eq3 = new ToggleAction("eq3", { power: 0, radius: 100, effects: [new AttributeEffect("initiative", 3)] }); + ship.actions.addCustom(eq3); + ship.actions.toggle(eq3, true); + + check.equals(battle.getAreaEffects(peer), [ + [drone1, drone1.effects[0]], + [ship, eq1.effects[0]], + ], "drone and toggle effects"); + }); + + test.case("is serializable", check => { + let battle = Battle.newQuickRandom(); + battle.ai_playing = true; + + let serializer = new Serializer(TK.SpaceTac); + let data = serializer.serialize(battle); + + let loaded = serializer.unserialize(data); + + check.equals(loaded.ai_playing, false, "ai playing is reset"); + battle.ai_playing = false; + check.equals(loaded, battle, "unserialized == initial"); + + let session = new GameSession(); + session.startNewGame(); + session.start_location.setupEncounter(); + session.start_location.enterLocation(session.player.fleet); + let battle1 = nn(session.getBattle()); + let data1 = serializer.serialize(battle1); + + let ratio = data.length / data1.length; + check.greaterorequal(ratio, 1.2, `quick battle serialized size (${data.length}) should be larger than campaign's (${data1.length})`); + }); + + test.case("can revert the last action", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + ship.setValue("hull", 13); + battle.log.clear(); + battle.log.add(new ShipValueDiff(ship, "hull", 4)); + battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); + battle.log.add(new ShipValueDiff(ship, "hull", 7)); + battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); + battle.log.add(new ShipValueDiff(ship, "hull", 2)); + + check.in("initial state", check => { + check.equals(ship.getValue("hull"), 13, "hull=13"); + check.equals(battle.log.count(), 5, "log count=5"); + }); + + battle.revertOneAction(); + + check.in("revert 1 action", check => { + check.equals(ship.getValue("hull"), 11, "hull=11"); + check.equals(battle.log.count(), 3, "log count=3"); + }); + + battle.revertOneAction(); + + check.in("revert 2 actions", check => { + check.equals(ship.getValue("hull"), 4, "hull=4"); + check.equals(battle.log.count(), 1, "log count=1"); + }); + + battle.revertOneAction(); + + check.in("revert 3 actions", check => { + check.equals(ship.getValue("hull"), 0, "hull=0"); + check.equals(battle.log.count(), 0, "log count=0"); + }); + }) +}); diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 51118bf..6758889 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -1,416 +1,436 @@ -module TK.SpaceTac { - /** - * A turn-based battle between fleets - */ - export class Battle { - // Grid for the arena - grid?: IArenaGrid +import { iarray, ichainit, ifilter, iforeach, imap, imaterialize } from "../common/Iterators" +import { RandomGenerator } from "../common/RandomGenerator" +import { RObjectContainer, RObjectId } from "../common/RObject" +import { bool, flatten } from "../common/Tools" +import { EndTurnAction } from "./actions/EndTurnAction" +import { AIWorker } from "./ai/AIWorker" +import { HexagonalArenaGrid, IArenaGrid } from "./ArenaGrid" +import { BattleChecks } from "./BattleChecks" +import { BattleLog, BattleLogClient } from "./BattleLog" +import { BattleOutcome } from "./BattleOutcome" +import { BattleStats } from "./BattleStats" +import { BaseBattleDiff } from "./diffs/BaseBattleDiff" +import { EndBattleDiff } from "./diffs/EndBattleDiff" +import { ShipActionEndedDiff } from "./diffs/ShipActionEndedDiff" +import { ShipActionUsedDiff } from "./diffs/ShipActionUsedDiff" +import { Drone } from "./Drone" +import { BaseEffect } from "./effects/BaseEffect" +import { Fleet } from "./Fleet" +import { Player } from "./Player" +import { Ship } from "./Ship" +import { Target } from "./Target" - // Battle outcome, if the battle has ended - outcome: BattleOutcome | null = null +/** + * A turn-based battle between fleets + */ +export class Battle { + // Grid for the arena + grid?: IArenaGrid - // Statistics - stats: BattleStats + // Battle outcome, if the battle has ended + outcome: BattleOutcome | null = null - // Log of all battle events - log: BattleLog + // Statistics + stats: BattleStats - // List of fleets engaged in battle - fleets: Fleet[] + // Log of all battle events + log: BattleLog - // Container of all engaged ships - ships: RObjectContainer + // List of fleets engaged in battle + fleets: Fleet[] - // List of playing ships, sorted by their initiative throw - play_order: Ship[] - play_index = -1 + // Container of all engaged ships + ships: RObjectContainer - // Current battle "cycle" (one cycle is one turn done for all ships in the play order) - cycle = 0 + // List of playing ships, sorted by their initiative throw + play_order: Ship[] + play_index = -1 - // List of deployed drones - drones = new RObjectContainer() + // Current battle "cycle" (one cycle is one turn done for all ships in the play order) + cycle = 0 - // Size of the battle area - width: number - height: number - border = 50 - ship_separation = 100 + // List of deployed drones + drones = new RObjectContainer() - // Indicator that an AI is playing - ai_playing = false + // Size of the battle area + width: number + height: number + border = 50 + ship_separation = 100 - constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) { - this.grid = new HexagonalArenaGrid(50); + // Indicator that an AI is playing + ai_playing = false - this.fleets = [fleet1, fleet2]; - this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships)); - this.play_order = []; - this.width = width; - this.height = height; + constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) { + this.grid = new HexagonalArenaGrid(50); - this.log = new BattleLog(); - this.stats = new BattleStats(); + this.fleets = [fleet1, fleet2]; + this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships)); + this.play_order = []; + this.width = width; + this.height = height; - this.fleets.forEach((fleet: Fleet) => { - fleet.setBattle(this); - }); - } + this.log = new BattleLog(); + this.stats = new BattleStats(); - postUnserialize() { - this.ai_playing = false; - } + this.fleets.forEach((fleet: Fleet) => { + fleet.setBattle(this); + }); + } - /** - * Property is true if the battle has ended - */ - get ended(): boolean { - return bool(this.outcome); - } + postUnserialize() { + this.ai_playing = false; + } - /** - * Apply a list of diffs to the game state, and add them to the log. - * - * This should be the main way to modify the game state. - */ - applyDiffs(diffs: BaseBattleDiff[]): void { - let client = new BattleLogClient(this, this.log); - diffs.forEach(diff => client.add(diff)); - } + /** + * Property is true if the battle has ended + */ + get ended(): boolean { + return bool(this.outcome); + } - /** - * Create a quick random battle, for testing purposes, or quick skirmish - */ - static newQuickRandom(start = true, level = 1, shipcount = 5): Battle { - let player1 = Player.newQuickRandom("Player", level, shipcount, true); - let player2 = Player.newQuickRandom("Enemy", level, shipcount, true); + /** + * Apply a list of diffs to the game state, and add them to the log. + * + * This should be the main way to modify the game state. + */ + applyDiffs(diffs: BaseBattleDiff[]): void { + let client = new BattleLogClient(this, this.log); + diffs.forEach(diff => client.add(diff)); + } - let result = new Battle(player1.fleet, player2.fleet); - if (start) { - result.start(); - } - return result; - } + /** + * Create a quick random battle, for testing purposes, or quick skirmish + */ + static newQuickRandom(start = true, level = 1, shipcount = 5): Battle { + let player1 = Player.newQuickRandom("Player", level, shipcount, true); + let player2 = Player.newQuickRandom("Enemy", level, shipcount, true); - /** - * Get the currently playing ship - */ - get playing_ship(): Ship | null { - return this.play_order[this.play_index] || null; - } - - /** - * Get a ship by its ID. - */ - getShip(id: RObjectId | null): Ship | null { - if (id === null) { - return null; - } else { - return this.ships.get(id); - } - } - - /** - * Return an iterator over all ships engaged in the battle - */ - iships(alive_only = false): Iterable { - let result = ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships))); - return alive_only ? ifilter(result, ship => ship.alive) : result; - } - - /** - * Return an iterator over ships allies of (or owned by) a player - */ - iallies(ship: Ship, alive_only = false): Iterable { - return ifilter(this.iships(alive_only), iship => iship.fleet.player.is(ship.fleet.player)); - } - - /** - * Return an iterator over ships enemy of a player - */ - ienemies(ship: Ship, alive_only = false): Iterable { - return ifilter(this.iships(alive_only), iship => !iship.fleet.player.is(ship.fleet.player)); - } - - /** - * Check if a player is able to play - * - * This can be used by the UI to determine if player interaction is allowed - */ - canPlay(player: Player): boolean { - if (this.ended) { - return false; - } else if (this.playing_ship && player.is(this.playing_ship.fleet.player)) { - return this.playing_ship.isAbleToPlay(false); - } else { - return false; - } - } - - // Create play order, performing an initiative throw - throwInitiative(gen: RandomGenerator = new RandomGenerator()): void { - var play_order: Ship[] = []; - - // Throw each ship's initiative - this.fleets.forEach(function (fleet: Fleet) { - fleet.ships.forEach(function (ship: Ship) { - ship.throwInitiative(gen); - play_order.push(ship); - }); - }); - - // Sort by throw result - play_order.sort(function (ship1: Ship, ship2: Ship) { - return (ship2.play_priority - ship1.play_priority); - }); - this.play_order = play_order; - this.play_index = -1; - } - - /** - * Get the number of turns before a specific ship plays (currently playing ship will return 0). - * - * Returns -1 if the ship is not in the play list. - */ - getPlayOrder(ship: Ship): number { - let index = this.play_order.indexOf(ship); - if (index < 0) { - return -1; - } else { - let result = index - this.play_index; - return (result < 0) ? (result + this.play_order.length) : result; - } - } - - /** - * Add a ship in the play order list - */ - removeFromPlayOrder(idx: number): void { - this.play_order.splice(idx, 1); - if (idx <= this.play_index) { - this.play_index -= 1; - } - } - - /** - * Remove a ship from the play order list - */ - insertInPlayOrder(idx: number, ship: Ship): void { - this.play_order.splice(idx, 0, ship); - if (idx <= this.play_index) { - this.play_index += 1; - } - } - - /** - * Set the currently playing ship - */ - setPlayingShip(ship: Ship): void { - let current = this.playing_ship; - if (current) { - current.playing = false; - } - - this.play_index = this.play_order.indexOf(ship); - this.ai_playing = false; - - current = this.playing_ship; - if (current) { - current.playing = true; - } - } - - // Defines the initial ship positions of all engaged fleets - placeShips(vertical = true): void { - if (vertical) { - this.placeFleetShips(this.fleets[0], this.width * 0.25, this.height * 0.5, 0, this.height); - this.placeFleetShips(this.fleets[1], this.width * 0.75, this.height * 0.5, Math.PI, this.height); - } else { - this.placeFleetShips(this.fleets[0], this.width * 0.5, this.height * 0.90, -Math.PI / 2, this.width); - this.placeFleetShips(this.fleets[1], this.width * 0.5, this.height * 0.10, Math.PI / 2, this.width); - } - } - - // Collect all ships within a given radius of a target - collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] { - return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius)); - } - - /** - * Ends the battle and sets the outcome - */ - endBattle(winner: Fleet | null) { - this.applyDiffs([new EndBattleDiff(winner, this.cycle)]); - } - - /** - * Get the next playing ship - */ - getNextShip(): Ship { - return this.play_order[(this.play_index + 1) % this.play_order.length]; - } - - /** - * Make an AI play the current ship - * - * This will run asynchronous work in background, until the playing ship is changed - */ - playAI(debug = false): boolean { - if (this.playing_ship && !this.ai_playing) { - this.ai_playing = true; - AIWorker.process(this, debug); - return true; - } else { - return false; - } - } - - /** - * Start the battle - * - * This will call all necessary initialization steps (initiative, placement...) - * - * This should not put any diff in the log - */ - start(): void { - this.outcome = null; - this.cycle = 1; - this.placeShips(); - iforeach(this.iships(), ship => ship.restoreInitialState()); - this.throwInitiative(); - this.setPlayingShip(this.play_order[0]); - } - - /** - * Force current ship's turn to end, then advance to the next one - */ - advanceToNextShip(): void { - if (this.playing_ship) { - this.applyOneAction(EndTurnAction.SINGLETON.id); - } else if (this.play_order.length) { - this.setPlayingShip(this.play_order[0]); - } - } - - /** - * Defines the initial ship positions for one fleet - * - * *x* and *y* are the center of the fleet formation - * *facing_angle* is the forward angle in radians - * *width* is the formation width - */ - private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number, width: number): void { - var side_angle = facing_angle + Math.PI * 0.5; - var spacing = width * 0.2; - var total_length = spacing * (fleet.ships.length - 1); - var dx = Math.cos(side_angle); - var dy = Math.sin(side_angle); - x -= dx * total_length * 0.5; - y -= dy * total_length * 0.5; - for (var i = 0; i < fleet.ships.length; i++) { - fleet.ships[i].setArenaPosition(x + i * dx * spacing, y + i * dy * spacing); - fleet.ships[i].setArenaFacingAngle(facing_angle); - } - } - - /** - * Add a drone to the battle - */ - addDrone(drone: Drone) { - this.drones.add(drone); - } - - /** - * Remove a drone from the battle - */ - removeDrone(drone: Drone) { - this.drones.remove(drone); - } - - /** - * Get the list of area effects that are expected to apply on a given ship - */ - getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] { - let drone_effects = this.drones.list().map(drone => { - // TODO Should apply filterImpactedShips from drone action - if (drone.isInRange(ship.arena_x, ship.arena_y)) { - return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]); - } else { - return []; - } - }); - - let ships_effects = this.ships.list().map(iship => { - return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]); - }); - - return flatten(drone_effects.concat(ships_effects)); - } - - /** - * Perform all battle checks to ensure the state is consistent - * - * Returns all applied diffs - */ - performChecks(): BaseBattleDiff[] { - let checks = new BattleChecks(this); - return checks.apply(); - } - - /** - * Apply one action to the battle state - * - * At the end of the action, some checks will be applied to ensure the battle state is consistent - */ - applyOneAction(action_id: RObjectId, target?: Target): boolean { - let ship = this.playing_ship; - if (ship) { - let action = ship.actions.getById(action_id); - if (action) { - if (!target) { - target = action.getDefaultTarget(ship); - } - - if (action.apply(this, ship, target)) { - this.performChecks(); - - if (!this.ended) { - this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]); - - if (ship.playing && ship.getValue("hull") <= 0) { - // Playing ship died during its action, force a turn end - this.applyOneAction(EndTurnAction.SINGLETON.id); - } - } - - return true; - } else { - return false; - } - } else { - console.error("Action not found on ship", action_id, ship); - return false; - } - } else { - console.error("Cannot apply action - ship not playing", action_id, this); - return false; - } - } - - /** - * Revert the last applied action - * - * This will remove diffs from the log, so pay attention to other log clients! - */ - revertOneAction(): void { - let client = new BattleLogClient(this, this.log); - while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) { - client.backward(); - } - if (!client.atStart()) { - client.backward(); - } - client.truncate(); - } + let result = new Battle(player1.fleet, player2.fleet); + if (start) { + result.start(); } + return result; + } + + /** + * Get the currently playing ship + */ + get playing_ship(): Ship | null { + return this.play_order[this.play_index] || null; + } + + /** + * Get a ship by its ID. + */ + getShip(id: RObjectId | null): Ship | null { + if (id === null) { + return null; + } else { + return this.ships.get(id); + } + } + + /** + * Return an iterator over all ships engaged in the battle + */ + iships(alive_only = false): Iterable { + let result = ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships))); + return alive_only ? ifilter(result, ship => ship.alive) : result; + } + + /** + * Return an iterator over ships allies of (or owned by) a player + */ + iallies(ship: Ship, alive_only = false): Iterable { + return ifilter(this.iships(alive_only), iship => iship.fleet.player.is(ship.fleet.player)); + } + + /** + * Return an iterator over ships enemy of a player + */ + ienemies(ship: Ship, alive_only = false): Iterable { + return ifilter(this.iships(alive_only), iship => !iship.fleet.player.is(ship.fleet.player)); + } + + /** + * Check if a player is able to play + * + * This can be used by the UI to determine if player interaction is allowed + */ + canPlay(player: Player): boolean { + if (this.ended) { + return false; + } else if (this.playing_ship && player.is(this.playing_ship.fleet.player)) { + return this.playing_ship.isAbleToPlay(false); + } else { + return false; + } + } + + // Create play order, performing an initiative throw + throwInitiative(gen: RandomGenerator = new RandomGenerator()): void { + var play_order: Ship[] = []; + + // Throw each ship's initiative + this.fleets.forEach(function (fleet: Fleet) { + fleet.ships.forEach(function (ship: Ship) { + ship.throwInitiative(gen); + play_order.push(ship); + }); + }); + + // Sort by throw result + play_order.sort(function (ship1: Ship, ship2: Ship) { + return (ship2.play_priority - ship1.play_priority); + }); + this.play_order = play_order; + this.play_index = -1; + } + + /** + * Get the number of turns before a specific ship plays (currently playing ship will return 0). + * + * Returns -1 if the ship is not in the play list. + */ + getPlayOrder(ship: Ship): number { + let index = this.play_order.indexOf(ship); + if (index < 0) { + return -1; + } else { + let result = index - this.play_index; + return (result < 0) ? (result + this.play_order.length) : result; + } + } + + /** + * Add a ship in the play order list + */ + removeFromPlayOrder(idx: number): void { + this.play_order.splice(idx, 1); + if (idx <= this.play_index) { + this.play_index -= 1; + } + } + + /** + * Remove a ship from the play order list + */ + insertInPlayOrder(idx: number, ship: Ship): void { + this.play_order.splice(idx, 0, ship); + if (idx <= this.play_index) { + this.play_index += 1; + } + } + + /** + * Set the currently playing ship + */ + setPlayingShip(ship: Ship): void { + let current = this.playing_ship; + if (current) { + current.playing = false; + } + + this.play_index = this.play_order.indexOf(ship); + this.ai_playing = false; + + current = this.playing_ship; + if (current) { + current.playing = true; + } + } + + // Defines the initial ship positions of all engaged fleets + placeShips(vertical = true): void { + if (vertical) { + this.placeFleetShips(this.fleets[0], this.width * 0.25, this.height * 0.5, 0, this.height); + this.placeFleetShips(this.fleets[1], this.width * 0.75, this.height * 0.5, Math.PI, this.height); + } else { + this.placeFleetShips(this.fleets[0], this.width * 0.5, this.height * 0.90, -Math.PI / 2, this.width); + this.placeFleetShips(this.fleets[1], this.width * 0.5, this.height * 0.10, Math.PI / 2, this.width); + } + } + + // Collect all ships within a given radius of a target + collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] { + return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius)); + } + + /** + * Ends the battle and sets the outcome + */ + endBattle(winner: Fleet | null) { + this.applyDiffs([new EndBattleDiff(winner, this.cycle)]); + } + + /** + * Get the next playing ship + */ + getNextShip(): Ship { + return this.play_order[(this.play_index + 1) % this.play_order.length]; + } + + /** + * Make an AI play the current ship + * + * This will run asynchronous work in background, until the playing ship is changed + */ + playAI(debug = false): boolean { + if (this.playing_ship && !this.ai_playing) { + this.ai_playing = true; + AIWorker.process(this, debug); + return true; + } else { + return false; + } + } + + /** + * Start the battle + * + * This will call all necessary initialization steps (initiative, placement...) + * + * This should not put any diff in the log + */ + start(): void { + this.outcome = null; + this.cycle = 1; + this.placeShips(); + iforeach(this.iships(), ship => ship.restoreInitialState()); + this.throwInitiative(); + this.setPlayingShip(this.play_order[0]); + } + + /** + * Force current ship's turn to end, then advance to the next one + */ + advanceToNextShip(): void { + if (this.playing_ship) { + this.applyOneAction(EndTurnAction.SINGLETON.id); + } else if (this.play_order.length) { + this.setPlayingShip(this.play_order[0]); + } + } + + /** + * Defines the initial ship positions for one fleet + * + * *x* and *y* are the center of the fleet formation + * *facing_angle* is the forward angle in radians + * *width* is the formation width + */ + private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number, width: number): void { + var side_angle = facing_angle + Math.PI * 0.5; + var spacing = width * 0.2; + var total_length = spacing * (fleet.ships.length - 1); + var dx = Math.cos(side_angle); + var dy = Math.sin(side_angle); + x -= dx * total_length * 0.5; + y -= dy * total_length * 0.5; + for (var i = 0; i < fleet.ships.length; i++) { + fleet.ships[i].setArenaPosition(x + i * dx * spacing, y + i * dy * spacing); + fleet.ships[i].setArenaFacingAngle(facing_angle); + } + } + + /** + * Add a drone to the battle + */ + addDrone(drone: Drone) { + this.drones.add(drone); + } + + /** + * Remove a drone from the battle + */ + removeDrone(drone: Drone) { + this.drones.remove(drone); + } + + /** + * Get the list of area effects that are expected to apply on a given ship + */ + getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] { + let drone_effects = this.drones.list().map(drone => { + // TODO Should apply filterImpactedShips from drone action + if (drone.isInRange(ship.arena_x, ship.arena_y)) { + return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]); + } else { + return []; + } + }); + + let ships_effects = this.ships.list().map(iship => { + return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]); + }); + + return flatten(drone_effects.concat(ships_effects)); + } + + /** + * Perform all battle checks to ensure the state is consistent + * + * Returns all applied diffs + */ + performChecks(): BaseBattleDiff[] { + let checks = new BattleChecks(this); + return checks.apply(); + } + + /** + * Apply one action to the battle state + * + * At the end of the action, some checks will be applied to ensure the battle state is consistent + */ + applyOneAction(action_id: RObjectId, target?: Target): boolean { + let ship = this.playing_ship; + if (ship) { + let action = ship.actions.getById(action_id); + if (action) { + if (!target) { + target = action.getDefaultTarget(ship); + } + + if (action.apply(this, ship, target)) { + this.performChecks(); + + if (!this.ended) { + this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]); + + if (ship.playing && ship.getValue("hull") <= 0) { + // Playing ship died during its action, force a turn end + this.applyOneAction(EndTurnAction.SINGLETON.id); + } + } + + return true; + } else { + return false; + } + } else { + console.error("Action not found on ship", action_id, ship); + return false; + } + } else { + console.error("Cannot apply action - ship not playing", action_id, this); + return false; + } + } + + /** + * Revert the last applied action + * + * This will remove diffs from the log, so pay attention to other log clients! + */ + revertOneAction(): void { + let client = new BattleLogClient(this, this.log); + while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) { + client.backward(); + } + if (!client.atStart()) { + client.backward(); + } + client.truncate(); + } } diff --git a/src/core/BattleCheats.spec.ts b/src/core/BattleCheats.spec.ts index 3fe3655..850a41d 100644 --- a/src/core/BattleCheats.spec.ts +++ b/src/core/BattleCheats.spec.ts @@ -1,25 +1,23 @@ -module TK.SpaceTac.Specs { - testing("BattleCheats", test => { - test.case("wins a battle", check => { - let battle = Battle.newQuickRandom(); - let cheats = new BattleCheats(battle, battle.fleets[0].player); +testing("BattleCheats", test => { + test.case("wins a battle", check => { + let battle = Battle.newQuickRandom(); + let cheats = new BattleCheats(battle, battle.fleets[0].player); - cheats.win(); + cheats.win(); - check.equals(battle.ended, true, "ended"); - check.same(nn(battle.outcome).winner, battle.fleets[0].id, "winner"); - check.equals(any(battle.fleets[1].ships, ship => ship.alive), false, "all enemies dead"); - }) + check.equals(battle.ended, true, "ended"); + check.same(nn(battle.outcome).winner, battle.fleets[0].id, "winner"); + check.equals(any(battle.fleets[1].ships, ship => ship.alive), false, "all enemies dead"); + }) - test.case("loses a battle", check => { - let battle = Battle.newQuickRandom(); - let cheats = new BattleCheats(battle, battle.fleets[0].player); + test.case("loses a battle", check => { + let battle = Battle.newQuickRandom(); + let cheats = new BattleCheats(battle, battle.fleets[0].player); - cheats.lose(); + cheats.lose(); - check.equals(battle.ended, true, "ended"); - check.same(nn(battle.outcome).winner, battle.fleets[1].id, "winner"); - check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead"); - }) - }) -} + check.equals(battle.ended, true, "ended"); + check.same(nn(battle.outcome).winner, battle.fleets[1].id, "winner"); + check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead"); + }) +}) diff --git a/src/core/BattleCheats.ts b/src/core/BattleCheats.ts index 11a179a..98f5567 100644 --- a/src/core/BattleCheats.ts +++ b/src/core/BattleCheats.ts @@ -1,40 +1,43 @@ -module TK.SpaceTac { - /** - * Cheat helpers for current battle - * - * May be used from the console to help development - */ - export class BattleCheats { - battle: Battle - player: Player +import { iforeach } from "../common/Iterators"; +import { first } from "../common/Tools"; +import { Battle } from "./Battle"; +import { Player } from "./Player"; - constructor(battle: Battle, player: Player) { - this.battle = battle; - this.player = player; - } +/** + * Cheat helpers for current battle + * + * May be used from the console to help development + */ +export class BattleCheats { + battle: Battle + player: Player - /** - * Make player win the current battle - */ - win(): void { - iforeach(this.battle.iships(), ship => { - if (!this.player.is(ship.fleet.player)) { - ship.setDead(); - } - }); - this.battle.endBattle(this.player.fleet); - } + constructor(battle: Battle, player: Player) { + this.battle = battle; + this.player = player; + } - /** - * Make player lose the current battle - */ - lose(): void { - iforeach(this.battle.iships(), ship => { - if (this.player.is(ship.fleet.player)) { - ship.setDead(); - } - }); - this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player))); - } - } + /** + * Make player win the current battle + */ + win(): void { + iforeach(this.battle.iships(), ship => { + if (!this.player.is(ship.fleet.player)) { + ship.setDead(); + } + }); + this.battle.endBattle(this.player.fleet); + } + + /** + * Make player lose the current battle + */ + lose(): void { + iforeach(this.battle.iships(), ship => { + if (this.player.is(ship.fleet.player)) { + ship.setDead(); + } + }); + this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player))); + } } diff --git a/src/core/BattleChecks.spec.ts b/src/core/BattleChecks.spec.ts index 8777bb7..dd414de 100644 --- a/src/core/BattleChecks.spec.ts +++ b/src/core/BattleChecks.spec.ts @@ -1,106 +1,104 @@ -module TK.SpaceTac.Specs { - testing("BattleChecks", test => { - test.case("detects victory conditions", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[1].addShip(); - let checks = new BattleChecks(battle); - check.equals(checks.checkVictory(), [], "no victory"); +testing("BattleChecks", test => { + test.case("detects victory conditions", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); + let checks = new BattleChecks(battle); + check.equals(checks.checkVictory(), [], "no victory"); - battle.cycle = 5; - ship1.setDead(); - check.equals(checks.checkVictory(), [new EndBattleDiff(battle.fleets[1], 5)], "victory"); - }) + battle.cycle = 5; + ship1.setDead(); + check.equals(checks.checkVictory(), [new EndBattleDiff(battle.fleets[1], 5)], "victory"); + }) - test.case("fixes ship values", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[1].addShip(); - let checks = new BattleChecks(battle); - check.equals(checks.checkShipValues(), [], "no value to fix"); + test.case("fixes ship values", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); + let checks = new BattleChecks(battle); + check.equals(checks.checkShipValues(), [], "no value to fix"); - ship1.setValue("hull", -4); - TestTools.setAttribute(ship2, "shield_capacity", 48); - ship2.setValue("shield", 60); - check.equals(checks.checkShipValues(), [ - new ShipValueDiff(ship1, "hull", 4), - new ShipValueDiff(ship2, "shield", -12), - ], "fixed values"); - }) + ship1.setValue("hull", -4); + TestTools.setAttribute(ship2, "shield_capacity", 48); + ship2.setValue("shield", 60); + check.equals(checks.checkShipValues(), [ + new ShipValueDiff(ship1, "hull", 4), + new ShipValueDiff(ship2, "shield", -12), + ], "fixed values"); + }) - test.case("marks ships as dead, except the playing one", check => { - let battle = TestTools.createBattle(1, 2); - let [ship1, ship2, ship3] = battle.play_order; - let checks = new BattleChecks(battle); - check.equals(checks.checkDeadShips(), [], "no ship to mark as dead"); + test.case("marks ships as dead, except the playing one", check => { + let battle = TestTools.createBattle(1, 2); + let [ship1, ship2, ship3] = battle.play_order; + let checks = new BattleChecks(battle); + check.equals(checks.checkDeadShips(), [], "no ship to mark as dead"); - battle.ships.list().forEach(ship => ship.setValue("hull", 0)); + battle.ships.list().forEach(ship => ship.setValue("hull", 0)); - let result = checks.checkDeadShips(); - check.equals(result, [new ShipDeathDiff(battle, ship2)], "ship2 marked as dead"); - battle.applyDiffs(result); + let result = checks.checkDeadShips(); + check.equals(result, [new ShipDeathDiff(battle, ship2)], "ship2 marked as dead"); + battle.applyDiffs(result); - result = checks.checkDeadShips(); - check.equals(result, [new ShipDeathDiff(battle, ship3)], "ship3 marked as dead"); - battle.applyDiffs(result); + result = checks.checkDeadShips(); + check.equals(result, [new ShipDeathDiff(battle, ship3)], "ship3 marked as dead"); + battle.applyDiffs(result); - result = checks.checkDeadShips(); - check.equals(result, [], "ship1 left playing"); - }) + result = checks.checkDeadShips(); + check.equals(result, [], "ship1 left playing"); + }) - test.case("fixes area effects", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[1].addShip(); - let checks = new BattleChecks(battle); + test.case("fixes area effects", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); + let checks = new BattleChecks(battle); - check.in("initial state", check => { - check.equals(checks.checkAreaEffects(), [], "effects diff"); - }); + check.in("initial state", check => { + check.equals(checks.checkAreaEffects(), [], "effects diff"); + }); - let effect1 = ship1.active_effects.add(new StickyEffect(new BaseEffect("e1"))); - let effect2 = ship1.active_effects.add(new BaseEffect("e2")); - let effect3 = ship1.active_effects.add(new BaseEffect("e3")); - check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship1, effect3]]); - check.in("sticky+obsolete+missing", check => { - check.equals(checks.checkAreaEffects(), [ - new ShipEffectRemovedDiff(ship1, effect2), - new ShipEffectAddedDiff(ship2, effect3) - ], "effects diff"); - }); - }) + let effect1 = ship1.active_effects.add(new StickyEffect(new BaseEffect("e1"))); + let effect2 = ship1.active_effects.add(new BaseEffect("e2")); + let effect3 = ship1.active_effects.add(new BaseEffect("e3")); + check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship1, effect3]]); + check.in("sticky+obsolete+missing", check => { + check.equals(checks.checkAreaEffects(), [ + new ShipEffectRemovedDiff(ship1, effect2), + new ShipEffectAddedDiff(ship2, effect3) + ], "effects diff"); + }); + }) - test.case("applies vigilance actions", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - ship1.setArenaPosition(100, 100); - TestTools.setShipModel(ship1, 10, 0, 5); - let ship2 = battle.fleets[1].addShip(); - ship2.setArenaPosition(1000, 1000); - TestTools.setShipModel(ship2, 10); - TestTools.setShipPlaying(battle, ship1); + test.case("applies vigilance actions", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + ship1.setArenaPosition(100, 100); + TestTools.setShipModel(ship1, 10, 0, 5); + let ship2 = battle.fleets[1].addShip(); + ship2.setArenaPosition(1000, 1000); + TestTools.setShipModel(ship2, 10); + TestTools.setShipPlaying(battle, ship1); - let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] })); - let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] })); - let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] })); - battle.applyOneAction(vig1.id); - battle.applyOneAction(vig2.id); - battle.applyOneAction(vig3.id); + let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] })); + let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] })); + let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] })); + battle.applyOneAction(vig1.id); + battle.applyOneAction(vig2.id); + battle.applyOneAction(vig3.id); - let checks = new BattleChecks(battle); - check.in("initial state", check => { - check.equals(checks.checkAreaEffects(), [], "effects diff"); - }); + let checks = new BattleChecks(battle); + check.in("initial state", check => { + check.equals(checks.checkAreaEffects(), [], "effects diff"); + }); - ship2.setArenaPosition(100, 160); - check.in("ship2 moved in range", check => { - check.equals(checks.checkAreaEffects(), [ - new ShipEffectAddedDiff(ship2, vig1.effects[0]), - new VigilanceAppliedDiff(ship1, vig1, ship2), - new ShipDamageDiff(ship2, 1, 0), - new ShipValueDiff(ship2, "hull", -1), - ], "effects diff"); - }); - }) - }) -} + ship2.setArenaPosition(100, 160); + check.in("ship2 moved in range", check => { + check.equals(checks.checkAreaEffects(), [ + new ShipEffectAddedDiff(ship2, vig1.effects[0]), + new VigilanceAppliedDiff(ship1, vig1, ship2), + new ShipDamageDiff(ship2, 1, 0), + new ShipValueDiff(ship2, "hull", -1), + ], "effects diff"); + }); + }) +}) diff --git a/src/core/BattleChecks.ts b/src/core/BattleChecks.ts index 5fc5ca8..855fa08 100644 --- a/src/core/BattleChecks.ts +++ b/src/core/BattleChecks.ts @@ -1,164 +1,174 @@ -module TK.SpaceTac { - /** - * List of checks to apply at the end of an action, to ensure a correct battle state - * - * This is useful when the list of effects simulated by an action was missing something - * - * To fix the state, new diffs will be applied - */ - export class BattleChecks { - constructor(private battle: Battle) { - } +import { ifirst, iforeach, imaterialize } from "../common/Iterators"; +import { RObjectContainer } from "../common/RObject"; +import { any, first, flatten, keys } from "../common/Tools"; +import { Battle } from "./Battle"; +import { BaseBattleDiff } from "./diffs/BaseBattleDiff"; +import { EndBattleDiff } from "./diffs/EndBattleDiff"; +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff"; +import { ShipValueDiff } from "./diffs/ShipValueDiff"; +import { StickyEffect } from "./effects/StickyEffect"; +import { Ship } from "./Ship"; +import { SHIP_VALUES } from "./ShipValue"; - /** - * Apply all the checks - */ - apply(): BaseBattleDiff[] { - let all: BaseBattleDiff[] = []; - let diffs: BaseBattleDiff[]; - let loops = 0; +/** + * List of checks to apply at the end of an action, to ensure a correct battle state + * + * This is useful when the list of effects simulated by an action was missing something + * + * To fix the state, new diffs will be applied + */ +export class BattleChecks { + constructor(private battle: Battle) { + } - do { - diffs = this.checkAll(); + /** + * Apply all the checks + */ + apply(): BaseBattleDiff[] { + let all: BaseBattleDiff[] = []; + let diffs: BaseBattleDiff[]; + let loops = 0; - if (diffs.length > 0) { - //console.log("Battle checks diffs", diffs); - this.battle.applyDiffs(diffs); - all = all.concat(diffs); - } + do { + diffs = this.checkAll(); - loops += 1; - if (loops >= 1000) { - console.error("Battle checks stuck in infinite loop", diffs); - break; - } - } while (diffs.length > 0); + if (diffs.length > 0) { + //console.log("Battle checks diffs", diffs); + this.battle.applyDiffs(diffs); + all = all.concat(diffs); + } - return all; - } + loops += 1; + if (loops >= 1000) { + console.error("Battle checks stuck in infinite loop", diffs); + break; + } + } while (diffs.length > 0); - /** - * Get a list of diffs to apply to fix the battle state - * - * This may not contain ALL the diffs needed, and should be called again while it returns diffs. - */ - checkAll(): BaseBattleDiff[] { - let diffs: BaseBattleDiff[] = []; + return all; + } - if (this.battle.ended) { - return diffs; - } + /** + * Get a list of diffs to apply to fix the battle state + * + * This may not contain ALL the diffs needed, and should be called again while it returns diffs. + */ + checkAll(): BaseBattleDiff[] { + let diffs: BaseBattleDiff[] = []; - diffs = this.checkAreaEffects(); - if (diffs.length) { - return diffs; - } - - diffs = this.checkShipValues(); - if (diffs.length) { - return diffs; - } - - diffs = this.checkDeadShips(); - if (diffs.length) { - return diffs; - } - - diffs = this.checkVictory(); - if (diffs.length) { - return diffs; - } - - return []; - } - - /** - * Checks victory conditions, to put an end to the battle - */ - checkVictory(): BaseBattleDiff[] { - if (this.battle.ended) { - return []; - } - - let fleets = this.battle.fleets; - if (any(fleets, fleet => !fleet.isAlive())) { - const winner = first(fleets, fleet => fleet.isAlive()); - return [new EndBattleDiff(winner, this.battle.cycle)]; - } else { - return []; - } - } - - /** - * Check that ship values stays in their allowed range - */ - checkShipValues(): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - iforeach(this.battle.iships(true), ship => { - keys(SHIP_VALUES).forEach(valuename => { - let value = ship.getValue(valuename); - if (value < 0) { - result.push(new ShipValueDiff(ship, valuename, -value)); - } else { - let maximum = ship.getAttribute((valuename + "_capacity")); - if (value > maximum) { - result.push(new ShipValueDiff(ship, valuename, maximum - value)); - } - } - }); - }); - - return result; - } - - /** - * Check that not-playing ships with no more hull are dead - */ - checkDeadShips(): BaseBattleDiff[] { - // We do one ship at a time, because the state of one ship may depend on another - let dying = ifirst(this.battle.iships(true), ship => !ship.playing && ship.getValue("hull") <= 0); - - if (dying) { - return dying.getDeathDiffs(this.battle); - } else { - return []; - } - } - - /** - * Get the diffs to apply to a ship, if moving at a given location - */ - getAreaEffectsDiff(ship: Ship): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - let expected = this.battle.getAreaEffects(ship); - let expected_hash = new RObjectContainer(expected.map(x => x[1])); - - // Remove obsolete effects - ship.active_effects.list().forEach(effect => { - if (!(effect instanceof StickyEffect) && !expected_hash.get(effect.id)) { - result.push(new ShipEffectRemovedDiff(ship, effect)); - result = result.concat(effect.getOffDiffs(ship)); - } - }); - - // Add missing effects - expected.forEach(([source, effect]) => { - if (!ship.active_effects.get(effect.id)) { - result.push(new ShipEffectAddedDiff(ship, effect)); - result = result.concat(effect.getOnDiffs(ship, source)); - } - }); - - return result; - } - - /** - * Check area effects (remove obsolete ones, and add missing ones) - */ - checkAreaEffects(): BaseBattleDiff[] { - let ships = imaterialize(this.battle.iships(true)); - return flatten(ships.map(ship => this.getAreaEffectsDiff(ship))); - } + if (this.battle.ended) { + return diffs; } + + diffs = this.checkAreaEffects(); + if (diffs.length) { + return diffs; + } + + diffs = this.checkShipValues(); + if (diffs.length) { + return diffs; + } + + diffs = this.checkDeadShips(); + if (diffs.length) { + return diffs; + } + + diffs = this.checkVictory(); + if (diffs.length) { + return diffs; + } + + return []; + } + + /** + * Checks victory conditions, to put an end to the battle + */ + checkVictory(): BaseBattleDiff[] { + if (this.battle.ended) { + return []; + } + + let fleets = this.battle.fleets; + if (any(fleets, fleet => !fleet.isAlive())) { + const winner = first(fleets, fleet => fleet.isAlive()); + return [new EndBattleDiff(winner, this.battle.cycle)]; + } else { + return []; + } + } + + /** + * Check that ship values stays in their allowed range + */ + checkShipValues(): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + iforeach(this.battle.iships(true), ship => { + keys(SHIP_VALUES).forEach(valuename => { + let value = ship.getValue(valuename); + if (value < 0) { + result.push(new ShipValueDiff(ship, valuename, -value)); + } else { + let maximum = ship.getAttribute((valuename + "_capacity")); + if (value > maximum) { + result.push(new ShipValueDiff(ship, valuename, maximum - value)); + } + } + }); + }); + + return result; + } + + /** + * Check that not-playing ships with no more hull are dead + */ + checkDeadShips(): BaseBattleDiff[] { + // We do one ship at a time, because the state of one ship may depend on another + let dying = ifirst(this.battle.iships(true), ship => !ship.playing && ship.getValue("hull") <= 0); + + if (dying) { + return dying.getDeathDiffs(this.battle); + } else { + return []; + } + } + + /** + * Get the diffs to apply to a ship, if moving at a given location + */ + getAreaEffectsDiff(ship: Ship): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + let expected = this.battle.getAreaEffects(ship); + let expected_hash = new RObjectContainer(expected.map(x => x[1])); + + // Remove obsolete effects + ship.active_effects.list().forEach(effect => { + if (!(effect instanceof StickyEffect) && !expected_hash.get(effect.id)) { + result.push(new ShipEffectRemovedDiff(ship, effect)); + result = result.concat(effect.getOffDiffs(ship)); + } + }); + + // Add missing effects + expected.forEach(([source, effect]) => { + if (!ship.active_effects.get(effect.id)) { + result.push(new ShipEffectAddedDiff(ship, effect)); + result = result.concat(effect.getOnDiffs(ship, source)); + } + }); + + return result; + } + + /** + * Check area effects (remove obsolete ones, and add missing ones) + */ + checkAreaEffects(): BaseBattleDiff[] { + let ships = imaterialize(this.battle.iships(true)); + return flatten(ships.map(ship => this.getAreaEffectsDiff(ship))); + } } diff --git a/src/core/BattleLog.ts b/src/core/BattleLog.ts index 3e89a9f..dfeaf3f 100644 --- a/src/core/BattleLog.ts +++ b/src/core/BattleLog.ts @@ -1,15 +1,14 @@ -/// +import { DiffLog, DiffLogClient } from "../common/DiffLog"; +import { Battle } from "./Battle"; -module TK.SpaceTac { - /** - * Log of diffs that change the state of a battle - */ - export class BattleLog extends DiffLog { - } - - /** - * Client for a battle log - */ - export class BattleLogClient extends DiffLogClient { - } +/** + * Log of diffs that change the state of a battle + */ +export class BattleLog extends DiffLog { +} + +/** + * Client for a battle log + */ +export class BattleLogClient extends DiffLogClient { } diff --git a/src/core/BattleOutcome.spec.ts b/src/core/BattleOutcome.spec.ts index 0a2939b..7510b8e 100644 --- a/src/core/BattleOutcome.spec.ts +++ b/src/core/BattleOutcome.spec.ts @@ -1,36 +1,34 @@ -module TK.SpaceTac.Specs { - testing("BattleOutcome", test => { - test.case("grants experience", check => { - let fleet1 = new Fleet(); - let ship1a = fleet1.addShip(new Ship()); - ship1a.level.forceLevel(3); - let ship1b = fleet1.addShip(new Ship()); - ship1b.level.forceLevel(4); - let fleet2 = new Fleet(); - let ship2a = fleet2.addShip(new Ship()); - ship2a.level.forceLevel(6); - let ship2b = fleet2.addShip(new Ship()); - ship2b.level.forceLevel(8); - check.equals(ship1a.level.getExperience(), 300); - check.equals(ship1b.level.getExperience(), 600); - check.equals(ship2a.level.getExperience(), 1500); - check.equals(ship2b.level.getExperience(), 2800); +testing("BattleOutcome", test => { + test.case("grants experience", check => { + let fleet1 = new Fleet(); + let ship1a = fleet1.addShip(new Ship()); + ship1a.level.forceLevel(3); + let ship1b = fleet1.addShip(new Ship()); + ship1b.level.forceLevel(4); + let fleet2 = new Fleet(); + let ship2a = fleet2.addShip(new Ship()); + ship2a.level.forceLevel(6); + let ship2b = fleet2.addShip(new Ship()); + ship2b.level.forceLevel(8); + check.equals(ship1a.level.getExperience(), 300); + check.equals(ship1b.level.getExperience(), 600); + check.equals(ship2a.level.getExperience(), 1500); + check.equals(ship2b.level.getExperience(), 2800); - // draw - let outcome = new BattleOutcome(null); - outcome.grantExperience([fleet1, fleet2]); - check.equals(ship1a.level.getExperience(), 345); - check.equals(ship1b.level.getExperience(), 645); - check.equals(ship2a.level.getExperience(), 1511); - check.equals(ship2b.level.getExperience(), 2811); + // draw + let outcome = new BattleOutcome(null); + outcome.grantExperience([fleet1, fleet2]); + check.equals(ship1a.level.getExperience(), 345); + check.equals(ship1b.level.getExperience(), 645); + check.equals(ship2a.level.getExperience(), 1511); + check.equals(ship2b.level.getExperience(), 2811); - // win/lose - outcome = new BattleOutcome(fleet1); - outcome.grantExperience([fleet1, fleet2]); - check.equals(ship1a.level.getExperience(), 480); - check.equals(ship1b.level.getExperience(), 780); - check.equals(ship2a.level.getExperience(), 1518); - check.equals(ship2b.level.getExperience(), 2818); - }); - }); -} + // win/lose + outcome = new BattleOutcome(fleet1); + outcome.grantExperience([fleet1, fleet2]); + check.equals(ship1a.level.getExperience(), 480); + check.equals(ship1b.level.getExperience(), 780); + check.equals(ship2a.level.getExperience(), 1518); + check.equals(ship2b.level.getExperience(), 2818); + }); +}); diff --git a/src/core/BattleOutcome.ts b/src/core/BattleOutcome.ts index bbde516..5d2cb25 100644 --- a/src/core/BattleOutcome.ts +++ b/src/core/BattleOutcome.ts @@ -1,34 +1,36 @@ -module TK.SpaceTac { - /** - * Result of an ended battle - * - * This stores the winner, and the retrievable loot - */ - export class BattleOutcome { - // Indicates if the battle is a draw (no winner) - draw: boolean +import { RObjectId } from "../common/RObject"; +import { flatten, sum } from "../common/Tools"; +import { Fleet } from "./Fleet"; - // Victorious fleet - winner: RObjectId | null +/** + * Result of an ended battle + * + * This stores the winner, and the retrievable loot + */ +export class BattleOutcome { + // Indicates if the battle is a draw (no winner) + draw: boolean - constructor(winner: Fleet | null) { - this.winner = winner ? winner.id : null; - this.draw = winner ? false : true; - } + // Victorious fleet + winner: RObjectId | null - /** - * Grant experience to participating fleets - */ - grantExperience(fleets: Fleet[]) { - fleets.forEach(fleet => { - let winfactor = (fleet.is(this.winner)) ? 0.03 : (this.draw ? 0.01 : 0.005); - let enemies = flatten(fleets.filter(f => f !== fleet).map(f => f.ships)); - let difficulty = sum(enemies.map(enemy => 100 + enemy.level.getExperience())); - fleet.ships.forEach(ship => { - ship.level.addExperience(Math.floor(difficulty * winfactor)); - ship.level.checkLevelUp(); - }); - }); - } - } + constructor(winner: Fleet | null) { + this.winner = winner ? winner.id : null; + this.draw = winner ? false : true; + } + + /** + * Grant experience to participating fleets + */ + grantExperience(fleets: Fleet[]) { + fleets.forEach(fleet => { + let winfactor = (fleet.is(this.winner)) ? 0.03 : (this.draw ? 0.01 : 0.005); + let enemies = flatten(fleets.filter(f => f !== fleet).map(f => f.ships)); + let difficulty = sum(enemies.map(enemy => 100 + enemy.level.getExperience())); + fleet.ships.forEach(ship => { + ship.level.addExperience(Math.floor(difficulty * winfactor)); + ship.level.checkLevelUp(); + }); + }); + } } diff --git a/src/core/BattleStats.spec.ts b/src/core/BattleStats.spec.ts index 63ba336..89f8ec5 100644 --- a/src/core/BattleStats.spec.ts +++ b/src/core/BattleStats.spec.ts @@ -1,75 +1,73 @@ -module TK.SpaceTac.Specs { - testing("BattleStats", test => { - test.case("collects stats", check => { - let stats = new BattleStats(); - check.equals(stats.stats, {}); +testing("BattleStats", test => { + test.case("collects stats", check => { + let stats = new BattleStats(); + check.equals(stats.stats, {}); - stats.addStat("Test", 1, true); - check.equals(stats.stats, { Test: [1, 0] }); + stats.addStat("Test", 1, true); + check.equals(stats.stats, { Test: [1, 0] }); - stats.addStat("Test", 1, true); - check.equals(stats.stats, { Test: [2, 0] }); + stats.addStat("Test", 1, true); + check.equals(stats.stats, { Test: [2, 0] }); - stats.addStat("Test", 1, false); - check.equals(stats.stats, { Test: [2, 1] }); + stats.addStat("Test", 1, false); + check.equals(stats.stats, { Test: [2, 1] }); - stats.addStat("Other Test", 10, true); - check.equals(stats.stats, { Test: [2, 1], "Other Test": [10, 0] }); - }) + stats.addStat("Other Test", 10, true); + check.equals(stats.stats, { Test: [2, 1], "Other Test": [10, 0] }); + }) - test.case("collects damage dealt", check => { - let stats = new BattleStats(); - let battle = new Battle(); - let attacker = battle.fleets[0].addShip(); - let defender = battle.fleets[1].addShip(); - stats.processLog(battle.log, battle.fleets[0]); - check.equals(stats.stats, {}); + test.case("collects damage dealt", check => { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.processLog(battle.log, battle.fleets[0]); + check.equals(stats.stats, {}); - battle.log.add(new ShipDamageDiff(defender, 1, 3, 2)); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Damage taken": [0, 1], "Damage shielded": [0, 3], "Damage evaded": [0, 2] }); + battle.log.add(new ShipDamageDiff(defender, 1, 3, 2)); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Damage taken": [0, 1], "Damage shielded": [0, 3], "Damage evaded": [0, 2] }); - battle.log.add(new ShipDamageDiff(attacker, 2, 1, 3)); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Damage taken": [2, 1], "Damage shielded": [1, 3], "Damage evaded": [3, 2] }); + battle.log.add(new ShipDamageDiff(attacker, 2, 1, 3)); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Damage taken": [2, 1], "Damage shielded": [1, 3], "Damage evaded": [3, 2] }); - battle.log.add(new ShipDamageDiff(defender, 1, 1, 1)); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Damage taken": [2, 2], "Damage shielded": [1, 4], "Damage evaded": [3, 3] }); - }) + battle.log.add(new ShipDamageDiff(defender, 1, 1, 1)); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Damage taken": [2, 2], "Damage shielded": [1, 4], "Damage evaded": [3, 3] }); + }) - test.case("collects distance moved", check => { - let stats = new BattleStats(); - let battle = new Battle(); - let attacker = battle.fleets[0].addShip(); - let defender = battle.fleets[1].addShip(); - stats.processLog(battle.log, battle.fleets[0]); - check.equals(stats.stats, {}); + test.case("collects distance moved", check => { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.processLog(battle.log, battle.fleets[0]); + check.equals(stats.stats, {}); - battle.log.add(new ShipMoveDiff(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0))); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Move distance (km)": [10, 0] }); + battle.log.add(new ShipMoveDiff(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0))); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Move distance (km)": [10, 0] }); - battle.log.add(new ShipMoveDiff(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63))); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Move distance (km)": [10, 58] }); - }) + battle.log.add(new ShipMoveDiff(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63))); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Move distance (km)": [10, 58] }); + }) - test.case("collects deployed drones", check => { - let stats = new BattleStats(); - let battle = new Battle(); - let attacker = battle.fleets[0].addShip(); - let defender = battle.fleets[1].addShip(); - stats.processLog(battle.log, battle.fleets[0]); - check.equals(stats.stats, {}); + test.case("collects deployed drones", check => { + let stats = new BattleStats(); + let battle = new Battle(); + let attacker = battle.fleets[0].addShip(); + let defender = battle.fleets[1].addShip(); + stats.processLog(battle.log, battle.fleets[0]); + check.equals(stats.stats, {}); - battle.log.add(new DroneDeployedDiff(new Drone(attacker))); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Drones deployed": [1, 0] }); + battle.log.add(new DroneDeployedDiff(new Drone(attacker))); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Drones deployed": [1, 0] }); - battle.log.add(new DroneDeployedDiff(new Drone(defender))); - stats.processLog(battle.log, battle.fleets[0], true); - check.equals(stats.stats, { "Drones deployed": [1, 1] }); - }) - }) -} + battle.log.add(new DroneDeployedDiff(new Drone(defender))); + stats.processLog(battle.log, battle.fleets[0], true); + check.equals(stats.stats, { "Drones deployed": [1, 1] }); + }) +}) diff --git a/src/core/BattleStats.ts b/src/core/BattleStats.ts index 9f2d4e1..4dfbd06 100644 --- a/src/core/BattleStats.ts +++ b/src/core/BattleStats.ts @@ -1,64 +1,70 @@ -module TK.SpaceTac { - /** - * Statistics collection over a battle - */ - export class BattleStats { - stats: { [name: string]: [number, number] } = {} +import { any, iteritems } from "../common/Tools"; +import { BattleLog } from "./BattleLog"; +import { BaseBattleShipDiff } from "./diffs/BaseBattleDiff"; +import { DroneDeployedDiff } from "./diffs/DroneDeployedDiff"; +import { ShipDamageDiff } from "./diffs/ShipDamageDiff"; +import { ShipMoveDiff } from "./diffs/ShipMoveDiff"; +import { Fleet } from "./Fleet"; - /** - * Add a value to the collector - */ - addStat(name: string, value: number, attacker: boolean) { - if (!this.stats[name]) { - this.stats[name] = [0, 0]; - } +/** + * Statistics collection over a battle + */ +export class BattleStats { + stats: { [name: string]: [number, number] } = {} - if (attacker) { - this.stats[name] = [this.stats[name][0] + value, this.stats[name][1]]; - } else { - this.stats[name] = [this.stats[name][0], this.stats[name][1] + value]; - } - } - - /** - * Get important stats - */ - getImportant(maxcount: number): { name: string, attacker: number, defender: number }[] { - // TODO Sort by importance - let result: { name: string, attacker: number, defender: number }[] = []; - iteritems(this.stats, (name, [attacker, defender]) => { - if (result.length < maxcount) { - result.push({ name: name, attacker: Math.round(attacker), defender: Math.round(defender) }); - } - }); - return result; - } - - /** - * Process a battle log - */ - processLog(log: BattleLog, attacker: Fleet, clear = true) { - if (clear) { - this.stats = {}; - } - - let n = log.count(); - for (let i = 0; i < n; i++) { - let diff = log.get(i); - if (diff instanceof BaseBattleShipDiff) { - let diff_ship = diff.ship_id; - let attacker_ship = any(attacker.ships, ship => ship.is(diff_ship)); - if (diff instanceof ShipDamageDiff) { - this.addStat("Damage evaded", diff.evaded, attacker_ship); - this.addStat("Damage shielded", diff.shield, attacker_ship); - this.addStat("Damage taken", diff.hull, attacker_ship); - } else if (diff instanceof ShipMoveDiff) { - this.addStat("Move distance (km)", diff.getDistance(), attacker_ship); - } else if (diff instanceof DroneDeployedDiff) { - this.addStat("Drones deployed", 1, attacker_ship); - } - } - } - } + /** + * Add a value to the collector + */ + addStat(name: string, value: number, attacker: boolean) { + if (!this.stats[name]) { + this.stats[name] = [0, 0]; } + + if (attacker) { + this.stats[name] = [this.stats[name][0] + value, this.stats[name][1]]; + } else { + this.stats[name] = [this.stats[name][0], this.stats[name][1] + value]; + } + } + + /** + * Get important stats + */ + getImportant(maxcount: number): { name: string, attacker: number, defender: number }[] { + // TODO Sort by importance + let result: { name: string, attacker: number, defender: number }[] = []; + iteritems(this.stats, (name, [attacker, defender]) => { + if (result.length < maxcount) { + result.push({ name: name, attacker: Math.round(attacker), defender: Math.round(defender) }); + } + }); + return result; + } + + /** + * Process a battle log + */ + processLog(log: BattleLog, attacker: Fleet, clear = true) { + if (clear) { + this.stats = {}; + } + + let n = log.count(); + for (let i = 0; i < n; i++) { + let diff = log.get(i); + if (diff instanceof BaseBattleShipDiff) { + let diff_ship = diff.ship_id; + let attacker_ship = any(attacker.ships, ship => ship.is(diff_ship)); + if (diff instanceof ShipDamageDiff) { + this.addStat("Damage evaded", diff.evaded, attacker_ship); + this.addStat("Damage shielded", diff.shield, attacker_ship); + this.addStat("Damage taken", diff.hull, attacker_ship); + } else if (diff instanceof ShipMoveDiff) { + this.addStat("Move distance (km)", diff.getDistance(), attacker_ship); + } else if (diff instanceof DroneDeployedDiff) { + this.addStat("Drones deployed", 1, attacker_ship); + } + } + } + } } diff --git a/src/core/Cooldown.spec.ts b/src/core/Cooldown.spec.ts index 602aeae..6022375 100644 --- a/src/core/Cooldown.spec.ts +++ b/src/core/Cooldown.spec.ts @@ -1,38 +1,36 @@ -module TK.SpaceTac.Specs { - testing("Cooldown", test => { - test.case("applies overheat and cooldown", check => { - let cooldown = new Cooldown(); - check.equals(cooldown.canUse(), true); +testing("Cooldown", test => { + test.case("applies overheat and cooldown", check => { + let cooldown = new Cooldown(); + check.equals(cooldown.canUse(), true); - cooldown.use(); - check.equals(cooldown.canUse(), true); + cooldown.use(); + check.equals(cooldown.canUse(), true); - cooldown.configure(2, 3); - check.equals(cooldown.canUse(), true); + cooldown.configure(2, 3); + check.equals(cooldown.canUse(), true); - cooldown.use(); - check.equals(cooldown.canUse(), true); + cooldown.use(); + check.equals(cooldown.canUse(), true); - cooldown.use(); - check.equals(cooldown.canUse(), false); + cooldown.use(); + check.equals(cooldown.canUse(), false); - cooldown.cool(); - check.equals(cooldown.canUse(), false); + cooldown.cool(); + check.equals(cooldown.canUse(), false); - cooldown.cool(); - check.equals(cooldown.canUse(), false); + cooldown.cool(); + check.equals(cooldown.canUse(), false); - cooldown.cool(); - check.equals(cooldown.canUse(), true); + cooldown.cool(); + check.equals(cooldown.canUse(), true); - cooldown.configure(1, 0); - check.equals(cooldown.canUse(), true); + cooldown.configure(1, 0); + check.equals(cooldown.canUse(), true); - cooldown.use(); - check.equals(cooldown.canUse(), false); + cooldown.use(); + check.equals(cooldown.canUse(), false); - cooldown.cool(); - check.equals(cooldown.canUse(), true); - }); - }); -} \ No newline at end of file + cooldown.cool(); + check.equals(cooldown.canUse(), true); + }); +}); diff --git a/src/core/Cooldown.ts b/src/core/Cooldown.ts index 0644468..4719663 100644 --- a/src/core/Cooldown.ts +++ b/src/core/Cooldown.ts @@ -1,93 +1,91 @@ -module TK.SpaceTac { - /** - * Cooldown system for equipments - */ - export class Cooldown { - // Number of uses in the current turn - uses = 0 +/** + * Cooldown system for equipments + */ +export class Cooldown { + // Number of uses in the current turn + uses = 0 - // Accumulated heat to dissipate (number of turns) - heat = 0 + // Accumulated heat to dissipate (number of turns) + heat = 0 - // Maximum number of uses allowed per turn before overheating (0 for unlimited) - overheat = 0 + // Maximum number of uses allowed per turn before overheating (0 for unlimited) + overheat = 0 - // Number of "end turn" needed to cooldown when overheated - cooling = 1 + // Number of "end turn" needed to cooldown when overheated + cooling = 1 - constructor(overheat = 0, cooling = 1) { - this.configure(overheat, cooling); - } + constructor(overheat = 0, cooling = 1) { + this.configure(overheat, cooling); + } - toString(): string { - return `Overheat ${this.overheat} / Cooldown ${this.cooling}`; - } + toString(): string { + return `Overheat ${this.overheat} / Cooldown ${this.cooling}`; + } - /** - * Check if the equipment can be used in regards to heat - */ - canUse(): boolean { - return this.heat == 0; - } + /** + * Check if the equipment can be used in regards to heat + */ + canUse(): boolean { + return this.heat == 0; + } - /** - * Check if the equipment would overheat if used - */ - willOverheat(): boolean { - return this.overheat > 0 && this.uses + 1 >= this.overheat; - } + /** + * Check if the equipment would overheat if used + */ + willOverheat(): boolean { + return this.overheat > 0 && this.uses + 1 >= this.overheat; + } - /** - * Check the number of uses before overheating - */ - getRemainingUses(): number { - if (this.overheat) { - return (this.heat > 0) ? 0 : (this.overheat - this.uses); - } else { - return Infinity; - } - } - - /** - * Configure the overheat and cooling - */ - configure(overheat: number, cooling: number) { - this.overheat = overheat; - this.cooling = Math.max(1, cooling); - this.reset(); - } - - /** - * Use the equipment, increasing the heat - */ - use(times = 1): void { - if (this.overheat) { - this.uses += times; - if (this.uses >= this.overheat) { - this.heat = this.cooling; - } else { - this.heat = 0; - } - } - } - - /** - * Apply one cooling-down step if necessary - */ - cool(steps = 1): void { - this.heat = Math.max(this.heat - steps, 0); - - if (this.heat == 0) { - this.uses = 0; - } - } - - /** - * Reset the cooldown (typically at the end of turn) - */ - reset(): void { - this.uses = 0; - this.heat = 0; - } + /** + * Check the number of uses before overheating + */ + getRemainingUses(): number { + if (this.overheat) { + return (this.heat > 0) ? 0 : (this.overheat - this.uses); + } else { + return Infinity; } + } + + /** + * Configure the overheat and cooling + */ + configure(overheat: number, cooling: number) { + this.overheat = overheat; + this.cooling = Math.max(1, cooling); + this.reset(); + } + + /** + * Use the equipment, increasing the heat + */ + use(times = 1): void { + if (this.overheat) { + this.uses += times; + if (this.uses >= this.overheat) { + this.heat = this.cooling; + } else { + this.heat = 0; + } + } + } + + /** + * Apply one cooling-down step if necessary + */ + cool(steps = 1): void { + this.heat = Math.max(this.heat - steps, 0); + + if (this.heat == 0) { + this.uses = 0; + } + } + + /** + * Reset the cooldown (typically at the end of turn) + */ + reset(): void { + this.uses = 0; + this.heat = 0; + } } diff --git a/src/core/Drone.spec.ts b/src/core/Drone.spec.ts index b922da3..6f8dc5b 100644 --- a/src/core/Drone.spec.ts +++ b/src/core/Drone.spec.ts @@ -1,62 +1,60 @@ -module TK.SpaceTac { - testing("Drone", test => { - test.case("applies area effects when deployed", check => { - let battle = TestTools.createBattle(); - let ship = nn(battle.playing_ship); - TestTools.setShipModel(ship, 100, 0, 10); - let weapon = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 300, drone_radius: 30, drone_effects: [new AttributeEffect("evasion", 15)] }); - ship.actions.addCustom(weapon); - let engine = TestTools.addEngine(ship, 1000); +testing("Drone", test => { + test.case("applies area effects when deployed", check => { + let battle = TestTools.createBattle(); + let ship = nn(battle.playing_ship); + TestTools.setShipModel(ship, 100, 0, 10); + let weapon = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 300, drone_radius: 30, drone_effects: [new AttributeEffect("evasion", 15)] }); + ship.actions.addCustom(weapon); + let engine = TestTools.addEngine(ship, 1000); - TestTools.actionChain(check, battle, [ - [ship, weapon, Target.newFromLocation(150, 50)], // deploy out of effects radius - [ship, engine, Target.newFromLocation(110, 50)], // move out of effects radius - [ship, engine, Target.newFromLocation(130, 50)], // move in effects radius - [ship, weapon, Target.newFromShip(ship)], // recall - [ship, weapon, Target.newFromLocation(130, 70)], // deploy in effects radius - ], [ - check => { - check.equals(ship.active_effects.count(), 0, "active effects"); - check.equals(ship.getValue("power"), 10, "power"); - check.equals(battle.drones.count(), 0, "drone count"); - }, - check => { - check.equals(ship.active_effects.count(), 0, "active effects"); - check.equals(ship.getValue("power"), 8, "power"); - check.equals(battle.drones.count(), 1, "drone count"); - }, - check => { - check.equals(ship.active_effects.count(), 0, "active effects"); - check.equals(ship.getValue("power"), 7, "power"); - check.equals(battle.drones.count(), 1, "drone count"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "active effects"); - check.equals(ship.getValue("power"), 6, "power"); - check.equals(battle.drones.count(), 1, "drone count"); - }, - check => { - check.equals(ship.active_effects.count(), 0, "active effects"); - check.equals(ship.getValue("power"), 8, "power"); - check.equals(battle.drones.count(), 0, "drone count"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "active effects"); - check.equals(ship.getValue("power"), 6, "power"); - check.equals(battle.drones.count(), 1, "drone count"); - }, - ]); - }); + TestTools.actionChain(check, battle, [ + [ship, weapon, Target.newFromLocation(150, 50)], // deploy out of effects radius + [ship, engine, Target.newFromLocation(110, 50)], // move out of effects radius + [ship, engine, Target.newFromLocation(130, 50)], // move in effects radius + [ship, weapon, Target.newFromShip(ship)], // recall + [ship, weapon, Target.newFromLocation(130, 70)], // deploy in effects radius + ], [ + check => { + check.equals(ship.active_effects.count(), 0, "active effects"); + check.equals(ship.getValue("power"), 10, "power"); + check.equals(battle.drones.count(), 0, "drone count"); + }, + check => { + check.equals(ship.active_effects.count(), 0, "active effects"); + check.equals(ship.getValue("power"), 8, "power"); + check.equals(battle.drones.count(), 1, "drone count"); + }, + check => { + check.equals(ship.active_effects.count(), 0, "active effects"); + check.equals(ship.getValue("power"), 7, "power"); + check.equals(battle.drones.count(), 1, "drone count"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "active effects"); + check.equals(ship.getValue("power"), 6, "power"); + check.equals(battle.drones.count(), 1, "drone count"); + }, + check => { + check.equals(ship.active_effects.count(), 0, "active effects"); + check.equals(ship.getValue("power"), 8, "power"); + check.equals(battle.drones.count(), 0, "drone count"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "active effects"); + check.equals(ship.getValue("power"), 6, "power"); + check.equals(battle.drones.count(), 1, "drone count"); + }, + ]); + }); - test.case("builds a textual description", check => { - let drone = new Drone(new Ship()); - check.equals(drone.getDescription(), "While deployed:\n• do nothing"); + test.case("builds a textual description", check => { + let drone = new Drone(new Ship()); + check.equals(drone.getDescription(), "While deployed:\n• do nothing"); - drone.effects = [ - new DamageEffect(5), - new AttributeEffect("evasion", 1) - ] - check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• evasion +1"); - }); - }); -} + drone.effects = [ + new DamageEffect(5), + new AttributeEffect("evasion", 1) + ] + check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• evasion +1"); + }); +}); diff --git a/src/core/Drone.ts b/src/core/Drone.ts index 6a8d067..186e1aa 100644 --- a/src/core/Drone.ts +++ b/src/core/Drone.ts @@ -1,63 +1,70 @@ -module TK.SpaceTac { - /** - * Drones are static objects that apply effects in a circular zone around themselves. - */ - export class Drone extends RObject { - // ID of the owning ship - owner: RObjectId +import { ifilter, imaterialize } from "../common/Iterators" +import { RObject, RObjectId } from "../common/RObject" +import { DeployDroneAction } from "./actions/DeployDroneAction" +import { ArenaLocation } from "./ArenaLocation" +import { Battle } from "./Battle" +import { BaseEffect } from "./effects/BaseEffect" +import { Ship } from "./Ship" +import { Target } from "./Target" - // Code of the drone - code: string +/** + * Drones are static objects that apply effects in a circular zone around themselves. + */ +export class Drone extends RObject { + // ID of the owning ship + owner: RObjectId - // Location in arena - x = 0 - y = 0 - radius = 0 + // Code of the drone + code: string - // Effects to apply - effects: BaseEffect[] = [] + // Location in arena + x = 0 + y = 0 + radius = 0 - // Action that triggered that drone - parent: DeployDroneAction | null = null; + // Effects to apply + effects: BaseEffect[] = [] - constructor(owner: Ship, code = "drone") { - super(); + // Action that triggered that drone + parent: DeployDroneAction | null = null; - this.owner = owner.id; - this.code = code; - } + constructor(owner: Ship, code = "drone") { + super(); - /** - * Return the current location of the drone - */ - get location(): ArenaLocation { - return new ArenaLocation(this.x, this.y); - } + this.owner = owner.id; + this.code = code; + } - /** - * Get a textual description of this drone - */ - getDescription(): string { - let effects = this.effects.map(effect => "• " + effect.getDescription()).join("\n"); - if (effects.length == 0) { - effects = "• do nothing"; - } - return `While deployed:\n${effects}`; - } + /** + * Return the current location of the drone + */ + get location(): ArenaLocation { + return new ArenaLocation(this.x, this.y); + } - /** - * Check if a location is in range - */ - isInRange(x: number, y: number): boolean { - return Target.newFromLocation(x, y).getDistanceTo(this) <= this.radius; - } - - /** - * Get the list of affected ships. - */ - getAffectedShips(battle: Battle): Ship[] { - let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius)); - return imaterialize(ships); - } + /** + * Get a textual description of this drone + */ + getDescription(): string { + let effects = this.effects.map(effect => "• " + effect.getDescription()).join("\n"); + if (effects.length == 0) { + effects = "• do nothing"; } -} \ No newline at end of file + return `While deployed:\n${effects}`; + } + + /** + * Check if a location is in range + */ + isInRange(x: number, y: number): boolean { + return Target.newFromLocation(x, y).getDistanceTo(this) <= this.radius; + } + + /** + * Get the list of affected ships. + */ + getAffectedShips(battle: Battle): Ship[] { + let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius)); + return imaterialize(ships); + } +} diff --git a/src/core/ExclusionAreas.spec.ts b/src/core/ExclusionAreas.spec.ts index 7700d96..44efebf 100644 --- a/src/core/ExclusionAreas.spec.ts +++ b/src/core/ExclusionAreas.spec.ts @@ -1,43 +1,46 @@ -module TK.SpaceTac.Specs { - testing("ExclusionAreas", test => { - test.case("constructs from a ship or battle", check => { - let battle = new Battle(); - battle.border = 17; - battle.ship_separation = 31; - let ship1 = battle.fleets[0].addShip(); - ship1.setArenaPosition(12, 5); - let ship2 = battle.fleets[1].addShip(); - ship2.setArenaPosition(43, 89); +import { testing } from "../common/Testing"; +import { ArenaLocationAngle } from "./ArenaLocation"; +import { Battle } from "./Battle"; +import { ExclusionAreas } from "./ExclusionAreas"; - let exclusion = ExclusionAreas.fromBattle(battle); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 31); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); +testing("ExclusionAreas", test => { + test.case("constructs from a ship or battle", check => { + let battle = new Battle(); + battle.border = 17; + battle.ship_separation = 31; + let ship1 = battle.fleets[0].addShip(); + ship1.setArenaPosition(12, 5); + let ship2 = battle.fleets[1].addShip(); + ship2.setArenaPosition(43, 89); - exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 120); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]); + let exclusion = ExclusionAreas.fromBattle(battle); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 31); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); - exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 31); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]); + exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 120); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]); - exclusion = ExclusionAreas.fromShip(ship1); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 31); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]); + exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 31); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]); - exclusion = ExclusionAreas.fromShip(ship2, 99); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 99); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]); + exclusion = ExclusionAreas.fromShip(ship1); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 31); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]); - exclusion = ExclusionAreas.fromShip(ship2, 10, false); - check.equals(exclusion.hard_border, 17); - check.equals(exclusion.effective_obstacle, 31); - check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); - }) - }) -} \ No newline at end of file + exclusion = ExclusionAreas.fromShip(ship2, 99); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 99); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]); + + exclusion = ExclusionAreas.fromShip(ship2, 10, false); + check.equals(exclusion.hard_border, 17); + check.equals(exclusion.effective_obstacle, 31); + check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); + }) +}) diff --git a/src/core/ExclusionAreas.ts b/src/core/ExclusionAreas.ts index 9befc2e..d6dc6fc 100644 --- a/src/core/ExclusionAreas.ts +++ b/src/core/ExclusionAreas.ts @@ -1,96 +1,101 @@ -module TK.SpaceTac { - /** - * Helper for working with exclusion areas (areas where a ship cannot go) - * - * There are three types of exclusion: - * - Hard border exclusion, that prevents a ship from being too close to the battle edges - * - Hard obstacle exclusion, that prevents two ships from being too close to each other - * - Soft obstacle exclusion, usually associated with an engine, that prevents a ship from moving too close to others - */ - export class ExclusionAreas { - xmin: number - xmax: number - ymin: number - ymax: number - active: boolean - hard_border = 50 - hard_obstacle = 100 - effective_obstacle = this.hard_obstacle - obstacles: ArenaLocation[] = [] +import { ifilter, imap, imaterialize } from "../common/Iterators" +import { cmp, contains, sorted } from "../common/Tools" +import { arenaDistance, ArenaLocation } from "./ArenaLocation" +import { Battle } from "./Battle" +import { Ship } from "./Ship" +import { Target } from "./Target" - constructor(width: number, height: number) { - this.xmin = 0; - this.xmax = width - 1; - this.ymin = 0; - this.ymax = height - 1; - this.active = width > 0 && height > 0; - } +/** + * Helper for working with exclusion areas (areas where a ship cannot go) + * + * There are three types of exclusion: + * - Hard border exclusion, that prevents a ship from being too close to the battle edges + * - Hard obstacle exclusion, that prevents two ships from being too close to each other + * - Soft obstacle exclusion, usually associated with an engine, that prevents a ship from moving too close to others + */ +export class ExclusionAreas { + xmin: number + xmax: number + ymin: number + ymax: number + active: boolean + hard_border = 50 + hard_obstacle = 100 + effective_obstacle = this.hard_obstacle + obstacles: ArenaLocation[] = [] - /** - * Build an exclusion helper from a battle. - */ - static fromBattle(battle: Battle, ignore_ships: Ship[] = [], soft_distance = 0): ExclusionAreas { - let result = new ExclusionAreas(battle.width, battle.height); - result.hard_border = battle.border; - result.hard_obstacle = battle.ship_separation; - let obstacles = imap(ifilter(battle.iships(true), ship => !contains(ignore_ships, ship)), ship => ship.location); - result.configure(imaterialize(obstacles), soft_distance); - return result; - } + constructor(width: number, height: number) { + this.xmin = 0; + this.xmax = width - 1; + this.ymin = 0; + this.ymax = height - 1; + this.active = width > 0 && height > 0; + } - /** - * Build an exclusion helper for a ship. - * - * If *ignore_self* is True, the ship will itself not be included in exclusion areas. - */ - static fromShip(ship: Ship, soft_distance = 0, ignore_self = true): ExclusionAreas { - let battle = ship.getBattle(); - if (battle) { - return ExclusionAreas.fromBattle(battle, ignore_self ? [ship] : [], soft_distance); - } else { - return new ExclusionAreas(0, 0); - } - } + /** + * Build an exclusion helper from a battle. + */ + static fromBattle(battle: Battle, ignore_ships: Ship[] = [], soft_distance = 0): ExclusionAreas { + let result = new ExclusionAreas(battle.width, battle.height); + result.hard_border = battle.border; + result.hard_obstacle = battle.ship_separation; + let obstacles = imap(ifilter(battle.iships(true), ship => !contains(ignore_ships, ship)), ship => ship.location); + result.configure(imaterialize(obstacles), soft_distance); + return result; + } - /** - * Configure the areas for next check calls. - */ - configure(obstacles: ArenaLocation[], soft_distance: number) { - this.obstacles = obstacles; - this.effective_obstacle = Math.max(soft_distance, this.hard_obstacle); - } - - /** - * Keep a location outside exclusion areas, when coming from a source. - * - * It will return the furthest location on the [source, location] segment, that is not inside an exclusion - * area. - */ - stopBefore(location: ArenaLocation, source: ArenaLocation): ArenaLocation { - if (!this.active) { - return location; - } - - let target = Target.newFromLocation(location.x, location.y); - - // Keep out of arena borders - target = target.keepInsideRectangle(this.xmin + this.hard_border, this.ymin + this.hard_border, - this.xmax - this.hard_border, this.ymax - this.hard_border, - source.x, source.y); - - // Apply collision prevention - let obstacles = sorted(this.obstacles, (a, b) => cmp(arenaDistance(a, source), arenaDistance(b, source), true)); - obstacles.forEach(s => { - let new_target = target.moveOutOfCircle(s.x, s.y, this.effective_obstacle, source.x, source.y); - if (target != new_target && arenaDistance(s, source) < this.effective_obstacle) { - // Already inside the nearest ship's exclusion area - target = Target.newFromLocation(source.x, source.y); - } else { - target = new_target; - } - }); - - return new ArenaLocation(target.x, target.y); - } + /** + * Build an exclusion helper for a ship. + * + * If *ignore_self* is True, the ship will itself not be included in exclusion areas. + */ + static fromShip(ship: Ship, soft_distance = 0, ignore_self = true): ExclusionAreas { + let battle = ship.getBattle(); + if (battle) { + return ExclusionAreas.fromBattle(battle, ignore_self ? [ship] : [], soft_distance); + } else { + return new ExclusionAreas(0, 0); } + } + + /** + * Configure the areas for next check calls. + */ + configure(obstacles: ArenaLocation[], soft_distance: number) { + this.obstacles = obstacles; + this.effective_obstacle = Math.max(soft_distance, this.hard_obstacle); + } + + /** + * Keep a location outside exclusion areas, when coming from a source. + * + * It will return the furthest location on the [source, location] segment, that is not inside an exclusion + * area. + */ + stopBefore(location: ArenaLocation, source: ArenaLocation): ArenaLocation { + if (!this.active) { + return location; + } + + let target = Target.newFromLocation(location.x, location.y); + + // Keep out of arena borders + target = target.keepInsideRectangle(this.xmin + this.hard_border, this.ymin + this.hard_border, + this.xmax - this.hard_border, this.ymax - this.hard_border, + source.x, source.y); + + // Apply collision prevention + let obstacles = sorted(this.obstacles, (a, b) => cmp(arenaDistance(a, source), arenaDistance(b, source), true)); + obstacles.forEach(s => { + let new_target = target.moveOutOfCircle(s.x, s.y, this.effective_obstacle, source.x, source.y); + if (target != new_target && arenaDistance(s, source) < this.effective_obstacle) { + // Already inside the nearest ship's exclusion area + target = Target.newFromLocation(source.x, source.y); + } else { + target = new_target; + } + }); + + return new ArenaLocation(target.x, target.y); + } } diff --git a/src/core/Fleet.spec.ts b/src/core/Fleet.spec.ts index 94fc765..1082459 100644 --- a/src/core/Fleet.spec.ts +++ b/src/core/Fleet.spec.ts @@ -1,168 +1,174 @@ -module TK.SpaceTac { - testing("Fleet", test => { - test.case("get average level", check => { - var fleet = new Fleet(); - check.equals(fleet.getLevel(), 0); +import { RObjectId } from "../common/RObject"; +import { testing } from "../common/Testing"; +import { Battle } from "./Battle"; +import { Fleet } from "./Fleet"; +import { Ship } from "./Ship"; +import { StarLocationType } from "./StarLocation"; +import { Universe } from "./Universe"; - fleet.addShip(new Ship()); - fleet.addShip(new Ship()); - fleet.addShip(new Ship()); +testing("Fleet", test => { + test.case("get average level", check => { + var fleet = new Fleet(); + check.equals(fleet.getLevel(), 0); - fleet.ships[0].level.forceLevel(2); - fleet.ships[1].level.forceLevel(4); - fleet.ships[2].level.forceLevel(7); - check.equals(fleet.getLevel(), 4); - }); + fleet.addShip(new Ship()); + fleet.addShip(new Ship()); + fleet.addShip(new Ship()); - test.case("adds and removes ships", check => { - let fleet1 = new Fleet(); - let fleet2 = new Fleet(); + fleet.ships[0].level.forceLevel(2); + fleet.ships[1].level.forceLevel(4); + fleet.ships[2].level.forceLevel(7); + check.equals(fleet.getLevel(), 4); + }); - let ship1 = fleet1.addShip(); - check.equals(fleet1.ships, [ship1]); - check.equals(fleet2.ships, []); + test.case("adds and removes ships", check => { + let fleet1 = new Fleet(); + let fleet2 = new Fleet(); - let ship2 = new Ship(); - check.equals(fleet1.ships, [ship1]); - check.equals(fleet2.ships, []); + let ship1 = fleet1.addShip(); + check.equals(fleet1.ships, [ship1]); + check.equals(fleet2.ships, []); - fleet2.addShip(ship2); - check.equals(fleet1.ships, [ship1]); - check.equals(fleet2.ships, [ship2]); + let ship2 = new Ship(); + check.equals(fleet1.ships, [ship1]); + check.equals(fleet2.ships, []); - fleet1.addShip(ship2); - check.equals(fleet1.ships, [ship1, ship2]); - check.equals(fleet2.ships, []); + fleet2.addShip(ship2); + check.equals(fleet1.ships, [ship1]); + check.equals(fleet2.ships, [ship2]); - fleet1.removeShip(ship1, fleet2); - check.equals(fleet1.ships, [ship2]); - check.equals(fleet2.ships, [ship1]); + fleet1.addShip(ship2); + check.equals(fleet1.ships, [ship1, ship2]); + check.equals(fleet2.ships, []); - fleet1.removeShip(ship1); - check.equals(fleet1.ships, [ship2]); - check.equals(fleet2.ships, [ship1]); + fleet1.removeShip(ship1, fleet2); + check.equals(fleet1.ships, [ship2]); + check.equals(fleet2.ships, [ship1]); - fleet1.removeShip(ship2); - check.equals(fleet1.ships, []); - check.equals(fleet2.ships, [ship1]); - }); + fleet1.removeShip(ship1); + check.equals(fleet1.ships, [ship2]); + check.equals(fleet2.ships, [ship1]); - test.case("changes location, only using jumps to travel between systems", check => { - let fleet = new Fleet(); - let universe = new Universe(); - let system1 = universe.addStar(); - let system2 = universe.addStar(); - let jump1 = system1.addLocation(StarLocationType.WARP); - let jump2 = system2.addLocation(StarLocationType.WARP); - jump1.setJumpDestination(jump2); - jump2.setJumpDestination(jump1); - let other1 = system1.addLocation(StarLocationType.PLANET); - universe.updateLocations(); + fleet1.removeShip(ship2); + check.equals(fleet1.ships, []); + check.equals(fleet2.ships, [ship1]); + }); - let result = fleet.move(other1); - check.in("cannot move from nowhere", check => { - check.equals(result, false); - check.equals(fleet.location, null); - }); + test.case("changes location, only using jumps to travel between systems", check => { + let fleet = new Fleet(); + let universe = new Universe(); + let system1 = universe.addStar(); + let system2 = universe.addStar(); + let jump1 = system1.addLocation(StarLocationType.WARP); + let jump2 = system2.addLocation(StarLocationType.WARP); + jump1.setJumpDestination(jump2); + jump2.setJumpDestination(jump1); + let other1 = system1.addLocation(StarLocationType.PLANET); + universe.updateLocations(); - fleet.setLocation(other1); - check.in("force set to other1", check => { - check.equals(fleet.location, other1.id); - }); - - result = fleet.move(jump2); - check.in("other1=>jump2", check => { - check.equals(result, false); - check.equals(fleet.location, other1.id); - }); - - result = fleet.move(jump1); - check.in("other1=>jump1", check => { - check.equals(result, true); - check.equals(fleet.location, jump1.id); - }); - - result = fleet.move(jump2); - check.in("jump1=>jump2", check => { - check.equals(result, true); - check.equals(fleet.location, jump2.id); - }); - - result = fleet.move(other1); - check.in("jump2=>other1", check => { - check.equals(result, false); - check.equals(fleet.location, jump2.id); - }); - - result = fleet.move(jump1); - check.in("jump2=>jump1", check => { - check.equals(result, true); - check.equals(fleet.location, jump1.id); - }); - }); - - test.case("registers presence in locations, and keeps track of visited locations", check => { - let fleet = new Fleet(); - let universe = new Universe(); - let star = universe.addStar(); - let loc1 = star.addLocation(StarLocationType.PLANET); - let loc2 = star.addLocation(StarLocationType.PLANET); - let loc3 = star.addLocation(StarLocationType.PLANET); - universe.updateLocations(); - - function checks(desc: string, fleets1: Fleet[], fleets2: Fleet[], fleets3: Fleet[], visited: RObjectId[]) { - check.in(desc, check => { - check.equals(loc1.fleets, fleets1, "loc1 fleets"); - check.equals(loc2.fleets, fleets2, "loc2 fleets"); - check.equals(loc3.fleets, fleets3, "loc3 fleets"); - check.equals(fleet.visited, visited, "visited"); - }); - } - - checks("initial", [], [], [], []); - - fleet.setLocation(loc1); - checks("first move to loc1", [fleet], [], [], [loc1.id]); - - fleet.setLocation(loc1); - checks("already in loc1", [fleet], [], [], [loc1.id]); - - fleet.setLocation(loc2); - checks("first move to loc2", [], [fleet], [], [loc2.id, loc1.id]); - - fleet.setLocation(loc3); - checks("first move to loc3", [], [], [fleet], [loc3.id, loc2.id, loc1.id]); - - fleet.setLocation(loc2); - checks("go back to loc2", [], [fleet], [], [loc2.id, loc3.id, loc1.id]); - }); - - test.case("checks if a fleet is alive", check => { - let battle = new Battle(); - let fleet = battle.fleets[0]; - check.equals(fleet.isAlive(), false); - - let ship1 = fleet.addShip(); - check.equals(fleet.isAlive(), true); - - let ship2 = fleet.addShip(); - check.equals(fleet.isAlive(), true); - - ship1.setDead(); - check.equals(fleet.isAlive(), true); - - ship2.setDead(); - check.equals(fleet.isAlive(), false); - - let ship3 = fleet.addShip(); - check.equals(fleet.isAlive(), true); - - let ship4 = fleet.addShip(); - ship4.critical = true; - check.equals(fleet.isAlive(), true); - - ship4.setDead(); - check.equals(fleet.isAlive(), false); - }); + let result = fleet.move(other1); + check.in("cannot move from nowhere", check => { + check.equals(result, false); + check.equals(fleet.location, null); }); -} + + fleet.setLocation(other1); + check.in("force set to other1", check => { + check.equals(fleet.location, other1.id); + }); + + result = fleet.move(jump2); + check.in("other1=>jump2", check => { + check.equals(result, false); + check.equals(fleet.location, other1.id); + }); + + result = fleet.move(jump1); + check.in("other1=>jump1", check => { + check.equals(result, true); + check.equals(fleet.location, jump1.id); + }); + + result = fleet.move(jump2); + check.in("jump1=>jump2", check => { + check.equals(result, true); + check.equals(fleet.location, jump2.id); + }); + + result = fleet.move(other1); + check.in("jump2=>other1", check => { + check.equals(result, false); + check.equals(fleet.location, jump2.id); + }); + + result = fleet.move(jump1); + check.in("jump2=>jump1", check => { + check.equals(result, true); + check.equals(fleet.location, jump1.id); + }); + }); + + test.case("registers presence in locations, and keeps track of visited locations", check => { + let fleet = new Fleet(); + let universe = new Universe(); + let star = universe.addStar(); + let loc1 = star.addLocation(StarLocationType.PLANET); + let loc2 = star.addLocation(StarLocationType.PLANET); + let loc3 = star.addLocation(StarLocationType.PLANET); + universe.updateLocations(); + + function checks(desc: string, fleets1: Fleet[], fleets2: Fleet[], fleets3: Fleet[], visited: RObjectId[]) { + check.in(desc, check => { + check.equals(loc1.fleets, fleets1, "loc1 fleets"); + check.equals(loc2.fleets, fleets2, "loc2 fleets"); + check.equals(loc3.fleets, fleets3, "loc3 fleets"); + check.equals(fleet.visited, visited, "visited"); + }); + } + + checks("initial", [], [], [], []); + + fleet.setLocation(loc1); + checks("first move to loc1", [fleet], [], [], [loc1.id]); + + fleet.setLocation(loc1); + checks("already in loc1", [fleet], [], [], [loc1.id]); + + fleet.setLocation(loc2); + checks("first move to loc2", [], [fleet], [], [loc2.id, loc1.id]); + + fleet.setLocation(loc3); + checks("first move to loc3", [], [], [fleet], [loc3.id, loc2.id, loc1.id]); + + fleet.setLocation(loc2); + checks("go back to loc2", [], [fleet], [], [loc2.id, loc3.id, loc1.id]); + }); + + test.case("checks if a fleet is alive", check => { + let battle = new Battle(); + let fleet = battle.fleets[0]; + check.equals(fleet.isAlive(), false); + + let ship1 = fleet.addShip(); + check.equals(fleet.isAlive(), true); + + let ship2 = fleet.addShip(); + check.equals(fleet.isAlive(), true); + + ship1.setDead(); + check.equals(fleet.isAlive(), true); + + ship2.setDead(); + check.equals(fleet.isAlive(), false); + + let ship3 = fleet.addShip(); + check.equals(fleet.isAlive(), true); + + let ship4 = fleet.addShip(); + ship4.critical = true; + check.equals(fleet.isAlive(), true); + + ship4.setDead(); + check.equals(fleet.isAlive(), false); + }); +}); diff --git a/src/core/Fleet.ts b/src/core/Fleet.ts index c8a86ec..b44f8b3 100644 --- a/src/core/Fleet.ts +++ b/src/core/Fleet.ts @@ -1,151 +1,156 @@ -module TK.SpaceTac { - /** - * A fleet of ships, all belonging to the same player - */ - export class Fleet extends RObject { - // Fleet owner - player: Player +import { RObject, RObjectId } from "../common/RObject" +import { add, any, remove } from "../common/Tools" +import { Battle } from "./Battle" +import { Player } from "./Player" +import { Ship } from "./Ship" +import { StarLocation, StarLocationType } from "./StarLocation" - // Fleet name - name: string +/** + * A fleet of ships, all belonging to the same player + */ +export class Fleet extends RObject { + // Fleet owner + player: Player - // List of ships - ships: Ship[] + // Fleet name + name: string - // Current fleet location - location: RObjectId | null = null + // List of ships + ships: Ship[] - // Visited locations (ordered by last visited) - visited: RObjectId[] = [] + // Current fleet location + location: RObjectId | null = null - // Current battle in which the fleet is engaged (null if not fighting) - battle: Battle | null = null + // Visited locations (ordered by last visited) + visited: RObjectId[] = [] - // Amount of credits available - credits = 0 + // Current battle in which the fleet is engaged (null if not fighting) + battle: Battle | null = null - // Create a fleet, bound to a player - constructor(player = new Player()) { - super(); + // Amount of credits available + credits = 0 - this.player = player; - this.name = player ? player.name : "Fleet"; - this.ships = []; - } + // Create a fleet, bound to a player + constructor(player = new Player()) { + super(); - jasmineToString(): string { - return `${this.name} [${this.ships.map(ship => ship.getName()).join(",")}]`; - } + this.player = player; + this.name = player ? player.name : "Fleet"; + this.ships = []; + } - /** - * Set the owner player - */ - setPlayer(player: Player): void { - this.player = player; - } + jasmineToString(): string { + return `${this.name} [${this.ships.map(ship => ship.getName()).join(",")}]`; + } - /** - * Set a location as visited - */ - setVisited(location: StarLocation): void { - remove(this.visited, location.id); - this.visited.unshift(location.id); - } + /** + * Set the owner player + */ + setPlayer(player: Player): void { + this.player = player; + } - /** - * Move the fleet to another location, checking that the move is physically possible - * - * Returns true on success - */ - move(to: StarLocation): boolean { - if (!this.location) { - return false; - } + /** + * Set a location as visited + */ + setVisited(location: StarLocation): void { + remove(this.visited, location.id); + this.visited.unshift(location.id); + } - let source = to.universe.locations.get(this.location); - if (!source) { - return false; - } - - if (source.star != to.star) { - // Need to jump, check conditions - if (source.type != StarLocationType.WARP || source.jump_dest != to) { - return false; - } - } - - this.setLocation(to); - return true; - } - - /** - * Set the current location of the fleet, without condition - */ - setLocation(location: StarLocation): void { - if (this.location) { - let previous = location.universe.locations.get(this.location); - if (previous) { - previous.removeFleet(this); - } - } - - this.location = location.id; - this.setVisited(location); - location.addFleet(this); - } - - /** - * Add a ship this fleet - */ - addShip(ship = new Ship(null, `${this.name} ${this.ships.length + 1}`)): Ship { - if (ship.fleet && ship.fleet != this) { - remove(ship.fleet.ships, ship); - } - add(this.ships, ship); - ship.fleet = this; - if (this.battle) { - this.battle.ships.add(ship); - } - return ship; - } - - /** - * Remove the ship from this fleet, transferring it to another fleet - */ - removeShip(ship: Ship, fleet = new Fleet()): void { - if (ship.fleet === this) { - fleet.addShip(ship); - } - } - - // Set the current battle - setBattle(battle: Battle | null): void { - this.battle = battle; - } - - // Get the average level of this fleet - getLevel(): number { - if (this.ships.length === 0) { - return 0; - } - - var sum = 0; - this.ships.forEach((ship: Ship) => { - sum += ship.level.get(); - }); - var avg = sum / this.ships.length; - return Math.floor(avg); - } - - /** - * Check if the fleet is considered alive (at least one ship alive, and no critical ship dead) - */ - isAlive(): boolean { - if (any(this.ships, ship => ship.critical && !ship.alive)) { - return false; - } else { - return any(this.ships, ship => ship.alive); - } - } + /** + * Move the fleet to another location, checking that the move is physically possible + * + * Returns true on success + */ + move(to: StarLocation): boolean { + if (!this.location) { + return false; } + + let source = to.universe.locations.get(this.location); + if (!source) { + return false; + } + + if (source.star != to.star) { + // Need to jump, check conditions + if (source.type != StarLocationType.WARP || source.jump_dest != to) { + return false; + } + } + + this.setLocation(to); + return true; + } + + /** + * Set the current location of the fleet, without condition + */ + setLocation(location: StarLocation): void { + if (this.location) { + let previous = location.universe.locations.get(this.location); + if (previous) { + previous.removeFleet(this); + } + } + + this.location = location.id; + this.setVisited(location); + location.addFleet(this); + } + + /** + * Add a ship this fleet + */ + addShip(ship = new Ship(null, `${this.name} ${this.ships.length + 1}`)): Ship { + if (ship.fleet && ship.fleet != this) { + remove(ship.fleet.ships, ship); + } + add(this.ships, ship); + ship.fleet = this; + if (this.battle) { + this.battle.ships.add(ship); + } + return ship; + } + + /** + * Remove the ship from this fleet, transferring it to another fleet + */ + removeShip(ship: Ship, fleet = new Fleet()): void { + if (ship.fleet === this) { + fleet.addShip(ship); + } + } + + // Set the current battle + setBattle(battle: Battle | null): void { + this.battle = battle; + } + + // Get the average level of this fleet + getLevel(): number { + if (this.ships.length === 0) { + return 0; + } + + var sum = 0; + this.ships.forEach((ship: Ship) => { + sum += ship.level.get(); + }); + var avg = sum / this.ships.length; + return Math.floor(avg); + } + + /** + * Check if the fleet is considered alive (at least one ship alive, and no critical ship dead) + */ + isAlive(): boolean { + if (any(this.ships, ship => ship.critical && !ship.alive)) { + return false; + } else { + return any(this.ships, ship => ship.alive); + } + } } diff --git a/src/core/FleetGenerator.ts b/src/core/FleetGenerator.ts index 2e47981..51b030c 100644 --- a/src/core/FleetGenerator.ts +++ b/src/core/FleetGenerator.ts @@ -1,29 +1,34 @@ -module TK.SpaceTac { - // Generator of balanced ships to form a fleet - export class FleetGenerator { - // Random generator to use - random: RandomGenerator; +import { RandomGenerator } from "../common/RandomGenerator"; +import { range } from "../common/Tools"; +import { Fleet } from "./Fleet"; +import { ShipModel } from "./models/ShipModel"; +import { Player } from "./Player"; +import { ShipGenerator } from "./ShipGenerator"; - constructor(random = RandomGenerator.global) { - this.random = random; - } +// Generator of balanced ships to form a fleet +export class FleetGenerator { + // Random generator to use + random: RandomGenerator; - /** - * Generate a fleet of a given level - */ - generate(level: number, player?: Player, ship_count = 3, upgrade = false): Fleet { - var fleet = new Fleet(player); - var ship_generator = new ShipGenerator(this.random); + constructor(random = RandomGenerator.global) { + this.random = random; + } - let models = this.random.sample(ShipModel.getDefaultCollection(), ship_count); + /** + * Generate a fleet of a given level + */ + generate(level: number, player?: Player, ship_count = 3, upgrade = false): Fleet { + var fleet = new Fleet(player); + var ship_generator = new ShipGenerator(this.random); - range(ship_count).forEach(i => { - var ship = ship_generator.generate(level, models[i] || null, upgrade); - ship.name = ship.model.name; - fleet.addShip(ship); - }); + let models = this.random.sample(ShipModel.getDefaultCollection(), ship_count); - return fleet; - } - } + range(ship_count).forEach(i => { + var ship = ship_generator.generate(level, models[i] || null, upgrade); + ship.name = ship.model.name; + fleet.addShip(ship); + }); + + return fleet; + } } diff --git a/src/core/GameSession.spec.ts b/src/core/GameSession.spec.ts index ab52e1b..22a4a2b 100644 --- a/src/core/GameSession.spec.ts +++ b/src/core/GameSession.spec.ts @@ -1,148 +1,154 @@ -module TK.SpaceTac.Specs { - testing("GameSession", test => { - /** - * Compare two sessions - */ - function compare(session1: GameSession, session2: GameSession) { - test.check.equals(session1, session2); - } +import { SkewedRandomGenerator } from "../common/RandomGenerator"; +import { RObjectContainer } from "../common/RObject"; +import { testing } from "../common/Testing"; +import { nn } from "../common/Tools"; +import { Fleet } from "./Fleet"; +import { GameSession } from "./GameSession"; +import { StarLocation, StarLocationType } from "./StarLocation"; - /** - * Apply deterministic game steps - */ - function applyGameSteps(session: GameSession): void { - var battle = nn(session.getBattle()); - battle.advanceToNextShip(); - // TODO Make some fixed moves (AI?) - battle.endBattle(battle.fleets[0]); - } +testing("GameSession", test => { + /** + * Compare two sessions + */ + function compare(session1: GameSession, session2: GameSession) { + test.check.equals(session1, session2); + } - test.case("serializes to a string", check => { - var session = new GameSession(); - session.startQuickBattle(); + /** + * Apply deterministic game steps + */ + function applyGameSteps(session: GameSession): void { + var battle = nn(session.getBattle()); + battle.advanceToNextShip(); + // TODO Make some fixed moves (AI?) + battle.endBattle(battle.fleets[0]); + } - // Dump and reload - var dumped = session.saveToString(); - var loaded_session = GameSession.loadFromString(dumped); + test.case("serializes to a string", check => { + var session = new GameSession(); + session.startQuickBattle(); - // Check equality - compare(loaded_session, session); + // Dump and reload + var dumped = session.saveToString(); + var loaded_session = GameSession.loadFromString(dumped); - // Apply game steps - applyGameSteps(session); - applyGameSteps(loaded_session); + // Check equality + compare(loaded_session, session); - // Check equality after game steps - compare(loaded_session, session); - }); + // Apply game steps + applyGameSteps(session); + applyGameSteps(loaded_session); - test.case("generates a quick battle", check => { - var session = new GameSession(); - session.startQuickBattle(); + // Check equality after game steps + compare(loaded_session, session); + }); - check.same(session.getBattle(), session.player.fleet.battle, "battle"); - check.same(session.player.fleet, session.fleet, "fleet"); - check.same(nn(session.getBattle()).fleets[0], session.fleet, "attacker fleet"); - }); + test.case("generates a quick battle", check => { + var session = new GameSession(); + session.startQuickBattle(); - test.case("applies battle outcome to bound player", check => { - let session = new GameSession(); - check.equals(session.getBattle(), null); + check.same(session.getBattle(), session.player.fleet.battle, "battle"); + check.same(session.player.fleet, session.fleet, "fleet"); + check.same(nn(session.getBattle()).fleets[0], session.fleet, "attacker fleet"); + }); - let location1 = new StarLocation(); - let location2 = new StarLocation(location1.star); - session.universe.locations = new RObjectContainer([location1, location2]); + test.case("applies battle outcome to bound player", check => { + let session = new GameSession(); + check.equals(session.getBattle(), null); - // Victory case - location1.encounter = new Fleet(); - session.player.fleet.setLocation(location1); - check.notequals(session.getBattle(), null); - check.notequals(location1.encounter, null); + let location1 = new StarLocation(); + let location2 = new StarLocation(location1.star); + session.universe.locations = new RObjectContainer([location1, location2]); - let battle = nn(session.getBattle()); - battle.endBattle(session.player.fleet); - session.setBattleEnded(); - check.notequals(session.getBattle(), null); - check.equals(location1.encounter, null); + // Victory case + location1.encounter = new Fleet(); + session.player.fleet.setLocation(location1); + check.notequals(session.getBattle(), null); + check.notequals(location1.encounter, null); - // Defeat case - location2.encounter = new Fleet(); - session.player.fleet.setLocation(location2); - check.notequals(session.getBattle(), null); - check.notequals(location2.encounter, null); + let battle = nn(session.getBattle()); + battle.endBattle(session.player.fleet); + session.setBattleEnded(); + check.notequals(session.getBattle(), null); + check.equals(location1.encounter, null); - battle = nn(session.getBattle()); - battle.endBattle(null); - session.setBattleEnded(); - check.notequals(session.getBattle(), null); - check.notequals(location2.encounter, null); - }); + // Defeat case + location2.encounter = new Fleet(); + session.player.fleet.setLocation(location2); + check.notequals(session.getBattle(), null); + check.notequals(location2.encounter, null); - test.case("generates a new campaign", check => { - let session = new GameSession(); + battle = nn(session.getBattle()); + battle.endBattle(null); + session.setBattleEnded(); + check.notequals(session.getBattle(), null); + check.notequals(location2.encounter, null); + }); - session.startNewGame(false); - check.notequals(session.player, null); - check.equals(session.player.fleet.ships.length, 0); - check.equals(session.player.fleet.credits, 0); - check.equals(session.universe.stars.length, 50); - check.equals(session.getBattle(), null); - check.equals(session.start_location.shop, null); - check.equals(session.start_location.encounter, null); - check.equals(session.start_location.encounter_gen, true); + test.case("generates a new campaign", check => { + let session = new GameSession(); - session.setCampaignFleet(); - check.equals(session.player.fleet.ships.length, 2); - check.equals(session.player.fleet.credits, 0); - check.equals(session.player.fleet.location, session.start_location.id); - }); + session.startNewGame(false); + check.notequals(session.player, null); + check.equals(session.player.fleet.ships.length, 0); + check.equals(session.player.fleet.credits, 0); + check.equals(session.universe.stars.length, 50); + check.equals(session.getBattle(), null); + check.equals(session.start_location.shop, null); + check.equals(session.start_location.encounter, null); + check.equals(session.start_location.encounter_gen, true); - test.case("can revert battle", check => { - let session = new GameSession(); - let star = session.universe.addStar(); - let loc1 = star.addLocation(StarLocationType.PLANET); - loc1.clearEncounter(); - let loc2 = star.addLocation(StarLocationType.PLANET); - loc2.encounter_random = new SkewedRandomGenerator([0], true); - session.universe.updateLocations(); + session.setCampaignFleet(); + check.equals(session.player.fleet.ships.length, 2); + check.equals(session.player.fleet.credits, 0); + check.equals(session.player.fleet.location, session.start_location.id); + }); - session.fleet.setLocation(loc1); - check.in("init in loc1", check => { - check.equals(session.getBattle(), null, "bound battle"); - check.equals(session.fleet.location, loc1.id, "fleet location"); - check.equals(session.player.hasVisitedLocation(loc2), false, "visited"); - }); + test.case("can revert battle", check => { + let session = new GameSession(); + let star = session.universe.addStar(); + let loc1 = star.addLocation(StarLocationType.PLANET); + loc1.clearEncounter(); + let loc2 = star.addLocation(StarLocationType.PLANET); + loc2.encounter_random = new SkewedRandomGenerator([0], true); + session.universe.updateLocations(); - session.fleet.setLocation(loc2); - check.in("move to loc2", check => { - check.notequals(session.getBattle(), null, "bound battle"); - check.equals(session.fleet.location, loc2.id, "fleet location"); - check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); - }); - let enemy = loc2.encounter; - - session.revertBattle(); - check.in("reverted", check => { - check.equals(session.getBattle(), null, "bound battle"); - check.equals(session.fleet.location, loc1.id, "fleet location"); - check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); - }); - - session.fleet.setLocation(loc2); - check.in("move to loc2 again", check => { - check.notequals(session.getBattle(), null, "bound battle"); - check.equals(session.fleet.location, loc2.id, "fleet location"); - check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); - check.same(nn(session.getBattle()).fleets[1], nn(enemy), "same enemy"); - }); - }); - - /*test.case("can generate lots of new games", check => { - range(20).forEach(() => { - let session = new GameSession(); - session.startNewGame(); - check.equals(session.universe.stars.length, 50); - }); - });*/ + session.fleet.setLocation(loc1); + check.in("init in loc1", check => { + check.equals(session.getBattle(), null, "bound battle"); + check.equals(session.fleet.location, loc1.id, "fleet location"); + check.equals(session.player.hasVisitedLocation(loc2), false, "visited"); }); -} + + session.fleet.setLocation(loc2); + check.in("move to loc2", check => { + check.notequals(session.getBattle(), null, "bound battle"); + check.equals(session.fleet.location, loc2.id, "fleet location"); + check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); + }); + let enemy = loc2.encounter; + + session.revertBattle(); + check.in("reverted", check => { + check.equals(session.getBattle(), null, "bound battle"); + check.equals(session.fleet.location, loc1.id, "fleet location"); + check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); + }); + + session.fleet.setLocation(loc2); + check.in("move to loc2 again", check => { + check.notequals(session.getBattle(), null, "bound battle"); + check.equals(session.fleet.location, loc2.id, "fleet location"); + check.equals(session.player.hasVisitedLocation(loc2), true, "visited"); + check.same(nn(session.getBattle()).fleets[1], nn(enemy), "same enemy"); + }); + }); + + /*test.case("can generate lots of new games", check => { + range(20).forEach(() => { + let session = new GameSession(); + session.startNewGame(); + check.equals(session.universe.stars.length, 50); + }); + });*/ +}); diff --git a/src/core/GameSession.ts b/src/core/GameSession.ts index a55ca3f..f6fe943 100644 --- a/src/core/GameSession.ts +++ b/src/core/GameSession.ts @@ -1,205 +1,215 @@ -module TK.SpaceTac { - /** - * A game session, binding a universe and a player - * - * This represents the current state of game - */ - export class GameSession { - // "Hopefully" unique session id - id: string +import { NAMESPACE } from ".." +import { iforeach } from "../common/Iterators" +import { RandomGenerator } from "../common/RandomGenerator" +import { Serializer } from "../common/Serializer" +import { Battle } from "./Battle" +import { Fleet } from "./Fleet" +import { FleetGenerator } from "./FleetGenerator" +import { PersonalityReactions } from "./PersonalityReactions" +import { Player } from "./Player" +import { StarLocation } from "./StarLocation" +import { Universe } from "./Universe" - // Game universe - universe: Universe +/** + * A game session, binding a universe and a player + * + * This represents the current state of game + */ +export class GameSession { + // "Hopefully" unique session id + id: string - // Current connected player - player: Player + // Game universe + universe: Universe - // Personality reactions - reactions: PersonalityReactions + // Current connected player + player: Player - // Starting location - start_location: StarLocation + // Personality reactions + reactions: PersonalityReactions - // Indicator that the session is the primary one - primary = true + // Starting location + start_location: StarLocation - // Indicator of spectator mode - spectator = false + // Indicator that the session is the primary one + primary = true - // Indicator that introduction has been watched - introduced = false + // Indicator of spectator mode + spectator = false - constructor() { - this.id = RandomGenerator.global.id(20); - this.universe = new Universe(); - this.player = new Player(); - this.reactions = new PersonalityReactions(); - this.start_location = new StarLocation(); - } + // Indicator that introduction has been watched + introduced = false - /** - * Get the currently played fleet - */ - get fleet(): Fleet { - return this.player.fleet; - } + constructor() { + this.id = RandomGenerator.global.id(20); + this.universe = new Universe(); + this.player = new Player(); + this.reactions = new PersonalityReactions(); + this.start_location = new StarLocation(); + } - /** - * Get an indicative description of the session (to help identify game saves) - */ - getDescription(): string { - let level = this.player.fleet.getLevel(); - let ships = this.player.fleet.ships.length; - return `Level ${level} - ${ships} ships`; - } + /** + * Get the currently played fleet + */ + get fleet(): Fleet { + return this.player.fleet; + } - // Load a game state from a string - static loadFromString(serialized: string): GameSession { - var serializer = new Serializer(TK.SpaceTac); - return serializer.unserialize(serialized); - } + /** + * Get an indicative description of the session (to help identify game saves) + */ + getDescription(): string { + let level = this.player.fleet.getLevel(); + let ships = this.player.fleet.ships.length; + return `Level ${level} - ${ships} ships`; + } - // Serializes the game state to a string - saveToString(): string { - var serializer = new Serializer(TK.SpaceTac); - return serializer.serialize(this); - } + // Load a game state from a string + static loadFromString(serialized: string): GameSession { + var serializer = new Serializer(NAMESPACE); + return serializer.unserialize(serialized); + } - /** - * Generate a real single player game (campaign) - * - * If *fleet* is false, the player fleet will be empty, and needs to be set with *setCampaignFleet*. - */ - startNewGame(fleet = true, story = false): void { - this.universe = new Universe(); - this.universe.generate(); + // Serializes the game state to a string + saveToString(): string { + var serializer = new Serializer(NAMESPACE); + return serializer.serialize(this); + } - this.start_location = this.universe.getStartLocation(); - this.start_location.clearEncounter(); - this.start_location.removeShop(); + /** + * Generate a real single player game (campaign) + * + * If *fleet* is false, the player fleet will be empty, and needs to be set with *setCampaignFleet*. + */ + startNewGame(fleet = true, story = false): void { + this.universe = new Universe(); + this.universe.generate(); - this.player = new Player(); + this.start_location = this.universe.getStartLocation(); + this.start_location.clearEncounter(); + this.start_location.removeShop(); - this.reactions = new PersonalityReactions(); + this.player = new Player(); - if (fleet) { - this.setCampaignFleet(null, story); - } - } + this.reactions = new PersonalityReactions(); - /** - * Set the initial campaign fleet, null for a default fleet - * - * If *story* is true, the main story arc will be started. - */ - setCampaignFleet(fleet: Fleet | null = null, story = true) { - if (fleet) { - this.player.setFleet(fleet); - } else { - let fleet_generator = new FleetGenerator(); - this.player.fleet = fleet_generator.generate(1, this.player, 2); - } - - this.player.fleet.setLocation(this.start_location); - - if (story) { - this.player.missions.startMainStory(this.universe, this.player.fleet); - } - } - - /** - * Start a new "quick battle" game - */ - startQuickBattle(with_ai: boolean = false, level?: number, shipcount?: number): void { - this.player = new Player(); - this.universe = new Universe(); - - let battle = Battle.newQuickRandom(true, level || RandomGenerator.global.randInt(1, 10), shipcount); - this.player.setFleet(battle.fleets[0]); - this.player.setBattle(battle); - - this.reactions = new PersonalityReactions(); - } - - /** - * Get currently played battle, null when none is in progress - */ - getBattle(): Battle | null { - return this.player.getBattle(); - } - - /** - * Get the main fleet's location - */ - getLocation(): StarLocation { - return this.universe.getLocation(this.player.fleet.location) || new StarLocation(); - } - - /** - * Set the end of current battle. - * - * This will reset the fleet, grant experience, and create loot. - * - * The battle will still be bound to the session (exitBattle or revertBattle should be called after). - */ - setBattleEnded() { - let battle = this.getBattle(); - - if (battle && battle.ended && battle.outcome) { - // Generate experience - battle.outcome.grantExperience(battle.fleets); - - // Reset ships status - iforeach(battle.iships(), ship => ship.restoreInitialState()); - - // If the battle happened in a star location, keep it informed - let location = this.universe.getLocation(this.player.fleet.location); - if (location) { - location.resolveEncounter(battle.outcome); - } - } - } - - /** - * Exit the current battle unconditionally, if any - * - * This does not apply retreat penalties, or battle outcome, only unbind the battle from current session - */ - exitBattle(): void { - this.player.setBattle(null); - } - - /** - * Revert current battle, and put the player's fleet to its previous location, as if the battle never happened - */ - revertBattle(): void { - this.exitBattle(); - - let previous_location = this.universe.getLocation(this.fleet.visited[1]); - if (previous_location) { - this.fleet.setLocation(previous_location); - } - } - - /** - * Returns true if the session has an universe to explore (campaign mode) - */ - hasUniverse(): boolean { - return this.universe.stars.length > 0; - } - - /** - * Returns true if initial fleet creation has been done. - */ - isFleetCreated(): boolean { - return this.player.fleet.ships.length > 0; - } - - /** - * Returns true if campaign introduction has been watched - */ - isIntroViewed(): boolean { - return this.introduced; - } + if (fleet) { + this.setCampaignFleet(null, story); } + } + + /** + * Set the initial campaign fleet, null for a default fleet + * + * If *story* is true, the main story arc will be started. + */ + setCampaignFleet(fleet: Fleet | null = null, story = true) { + if (fleet) { + this.player.setFleet(fleet); + } else { + let fleet_generator = new FleetGenerator(); + this.player.fleet = fleet_generator.generate(1, this.player, 2); + } + + this.player.fleet.setLocation(this.start_location); + + if (story) { + this.player.missions.startMainStory(this.universe, this.player.fleet); + } + } + + /** + * Start a new "quick battle" game + */ + startQuickBattle(with_ai: boolean = false, level?: number, shipcount?: number): void { + this.player = new Player(); + this.universe = new Universe(); + + let battle = Battle.newQuickRandom(true, level || RandomGenerator.global.randInt(1, 10), shipcount); + this.player.setFleet(battle.fleets[0]); + this.player.setBattle(battle); + + this.reactions = new PersonalityReactions(); + } + + /** + * Get currently played battle, null when none is in progress + */ + getBattle(): Battle | null { + return this.player.getBattle(); + } + + /** + * Get the main fleet's location + */ + getLocation(): StarLocation { + return this.universe.getLocation(this.player.fleet.location) || new StarLocation(); + } + + /** + * Set the end of current battle. + * + * This will reset the fleet, grant experience, and create loot. + * + * The battle will still be bound to the session (exitBattle or revertBattle should be called after). + */ + setBattleEnded() { + let battle = this.getBattle(); + + if (battle && battle.ended && battle.outcome) { + // Generate experience + battle.outcome.grantExperience(battle.fleets); + + // Reset ships status + iforeach(battle.iships(), ship => ship.restoreInitialState()); + + // If the battle happened in a star location, keep it informed + let location = this.universe.getLocation(this.player.fleet.location); + if (location) { + location.resolveEncounter(battle.outcome); + } + } + } + + /** + * Exit the current battle unconditionally, if any + * + * This does not apply retreat penalties, or battle outcome, only unbind the battle from current session + */ + exitBattle(): void { + this.player.setBattle(null); + } + + /** + * Revert current battle, and put the player's fleet to its previous location, as if the battle never happened + */ + revertBattle(): void { + this.exitBattle(); + + let previous_location = this.universe.getLocation(this.fleet.visited[1]); + if (previous_location) { + this.fleet.setLocation(previous_location); + } + } + + /** + * Returns true if the session has an universe to explore (campaign mode) + */ + hasUniverse(): boolean { + return this.universe.stars.length > 0; + } + + /** + * Returns true if initial fleet creation has been done. + */ + isFleetCreated(): boolean { + return this.player.fleet.ships.length > 0; + } + + /** + * Returns true if campaign introduction has been watched + */ + isIntroViewed(): boolean { + return this.introduced; + } } diff --git a/src/core/MoveFireSimulator.spec.ts b/src/core/MoveFireSimulator.spec.ts index d34152c..63d6efa 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -1,207 +1,225 @@ -module TK.SpaceTac.Specs { - testing("MoveFireSimulator", test => { +import { iarray, imaterialize } from "../common/Iterators"; +import { testing } from "../common/Testing"; +import { nn } from "../common/Tools"; +import { BaseAction } from "./actions/BaseAction"; +import { MoveAction } from "./actions/MoveAction"; +import { TriggerAction } from "./actions/TriggerAction"; +import { ArenaLocationAngle } from "./ArenaLocation"; +import { Battle } from "./Battle"; +import { EndBattleDiff } from "./diffs/EndBattleDiff"; +import { ProjectileFiredDiff } from "./diffs/ProjectileFiredDiff"; +import { ShipActionUsedDiff } from "./diffs/ShipActionUsedDiff"; +import { ShipDamageDiff } from "./diffs/ShipDamageDiff"; +import { ShipDeathDiff } from "./diffs/ShipDeathDiff"; +import { ShipMoveDiff } from "./diffs/ShipMoveDiff"; +import { ShipValueDiff } from "./diffs/ShipValueDiff"; +import { ApproachSimulationError, MoveFireSimulator } from "./MoveFireSimulator"; +import { Ship } from "./Ship"; +import { Target } from "./Target"; +import { TestTools } from "./TestTools"; - function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] { - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, ship_ap); - TestTools.addEngine(ship, engine_distance); - let action = new TriggerAction("weapon", { power: weapon_ap, range: distance }); - let simulator = new MoveFireSimulator(ship); - return [ship, simulator, action]; - } +testing("MoveFireSimulator", test => { - test.case("finds a suitable engine to make an approach", check => { - let ship = new Ship(); - let simulator = new MoveFireSimulator(ship); - check.equals(simulator.findEngine(), null, "no engine"); - let engine1 = TestTools.addEngine(ship, 100); - engine1.configureCooldown(1, 1); - check.same(simulator.findEngine(), engine1, "one engine"); - let engine2 = TestTools.addEngine(ship, 120); - engine2.configureCooldown(1, 1); - check.same(simulator.findEngine(), engine1, "two engines, choose first one"); - ship.actions.storeUsage(engine1); - check.same(simulator.findEngine(), engine2, "first engine overheated, choose second one"); - ship.actions.storeUsage(engine2); - check.equals(simulator.findEngine(), null, "both engines overheated"); - }); + function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] { + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, ship_ap); + TestTools.addEngine(ship, engine_distance); + let action = new TriggerAction("weapon", { power: weapon_ap, range: distance }); + let simulator = new MoveFireSimulator(ship); + return [ship, simulator, action]; + } - test.case("fires directly when in range", check => { - let [ship, simulator, action] = simpleWeaponCase(); - let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); + test.case("finds a suitable engine to make an approach", check => { + let ship = new Ship(); + let simulator = new MoveFireSimulator(ship); + check.equals(simulator.findEngine(), null, "no engine"); + let engine1 = TestTools.addEngine(ship, 100); + engine1.configureCooldown(1, 1); + check.same(simulator.findEngine(), engine1, "one engine"); + let engine2 = TestTools.addEngine(ship, 120); + engine2.configureCooldown(1, 1); + check.same(simulator.findEngine(), engine1, "two engines, choose first one"); + ship.actions.storeUsage(engine1); + check.same(simulator.findEngine(), engine2, "first engine overheated, choose second one"); + ship.actions.storeUsage(engine2); + check.equals(simulator.findEngine(), null, "both engines overheated"); + }); - check.same(result.success, true, 'success'); - check.same(result.need_move, false, 'need_move'); - check.same(result.need_fire, true, 'need_fire'); - check.same(result.can_fire, true, 'can_fire'); - check.same(result.total_fire_ap, 3, 'total_fire_ap'); + test.case("fires directly when in range", check => { + let [ship, simulator, action] = simpleWeaponCase(); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); - check.equals(result.parts, [ - { action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: true } - ]); - }); + check.same(result.success, true, 'success'); + check.same(result.need_move, false, 'need_move'); + check.same(result.need_fire, true, 'need_fire'); + check.same(result.can_fire, true, 'can_fire'); + check.same(result.total_fire_ap, 3, 'total_fire_ap'); - test.case("can't fire when in range, but not enough AP", check => { - let [ship, simulator, action] = simpleWeaponCase(10, 2, 3); - let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); - check.same(result.success, true, 'success'); - check.same(result.need_move, false, 'need_move'); - check.same(result.need_fire, true, 'need_fire'); - check.same(result.can_fire, false, 'can_fire'); - check.same(result.total_fire_ap, 3, 'total_fire_ap'); + check.equals(result.parts, [ + { action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: true } + ]); + }); - check.equals(result.parts, [ - { action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: false } - ]); - }); + test.case("can't fire when in range, but not enough AP", check => { + let [ship, simulator, action] = simpleWeaponCase(10, 2, 3); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null)); + check.same(result.success, true, 'success'); + check.same(result.need_move, false, 'need_move'); + check.same(result.need_fire, true, 'need_fire'); + check.same(result.can_fire, false, 'can_fire'); + check.same(result.total_fire_ap, 3, 'total_fire_ap'); - test.case("moves straight to get within range", check => { - let [ship, simulator, action] = simpleWeaponCase(); - let result = simulator.simulateAction(action, new Target(ship.arena_x + 15, ship.arena_y, null)); - check.same(result.success, true, 'success'); - check.same(result.need_move, true, 'need_move'); - check.same(result.can_end_move, true, 'can_end_move'); - check.equals(result.move_location, new Target(ship.arena_x + 5, ship.arena_y, null)); - check.equals(result.total_move_ap, 1); - check.same(result.need_fire, true, 'need_fire'); - check.same(result.can_fire, true, 'can_fire'); - check.same(result.total_fire_ap, 3, 'total_fire_ap'); + check.equals(result.parts, [ + { action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: false } + ]); + }); - let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; - check.equals(result.parts, [ - { action: move_action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1, possible: true }, - { action: action, target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3, possible: true } - ]); - }); + test.case("moves straight to get within range", check => { + let [ship, simulator, action] = simpleWeaponCase(); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 15, ship.arena_y, null)); + check.same(result.success, true, 'success'); + check.same(result.need_move, true, 'need_move'); + check.same(result.can_end_move, true, 'can_end_move'); + check.equals(result.move_location, new Target(ship.arena_x + 5, ship.arena_y, null)); + check.equals(result.total_move_ap, 1); + check.same(result.need_fire, true, 'need_fire'); + check.same(result.can_fire, true, 'can_fire'); + check.same(result.total_fire_ap, 3, 'total_fire_ap'); - test.case("scans a circle for move targets", check => { - let simulator = new MoveFireSimulator(new Ship()); + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; + check.equals(result.parts, [ + { action: move_action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1, possible: true }, + { action: action, target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3, possible: true } + ]); + }); - let result = simulator.scanCircle(50, 30, 10, 1, 1); - check.equals(imaterialize(result), [ - new Target(50, 30) - ]); + test.case("scans a circle for move targets", check => { + let simulator = new MoveFireSimulator(new Ship()); - result = simulator.scanCircle(50, 30, 10, 2, 1); - check.equals(imaterialize(result), [ - new Target(50, 30), - new Target(60, 30) - ]); + let result = simulator.scanCircle(50, 30, 10, 1, 1); + check.equals(imaterialize(result), [ + new Target(50, 30) + ]); - result = simulator.scanCircle(50, 30, 10, 2, 2); - check.equals(imaterialize(result), [ - new Target(50, 30), - new Target(60, 30), - new Target(40, 30) - ]); + result = simulator.scanCircle(50, 30, 10, 2, 1); + check.equals(imaterialize(result), [ + new Target(50, 30), + new Target(60, 30) + ]); - result = simulator.scanCircle(50, 30, 10, 3, 4); - check.equals(imaterialize(result), [ - new Target(50, 30), - new Target(55, 30), - new Target(45, 30), - new Target(60, 30), - new Target(50, 40), - new Target(40, 30), - new Target(50, 20) - ]); - }); + result = simulator.scanCircle(50, 30, 10, 2, 2); + check.equals(imaterialize(result), [ + new Target(50, 30), + new Target(60, 30), + new Target(40, 30) + ]); - test.case("accounts for exclusion areas for the approach", check => { - let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50); - ship.setArenaPosition(300, 200); - let battle = new Battle(); - battle.fleets[0].addShip(ship); - let ship1 = battle.fleets[0].addShip(); - let moveaction = nn(simulator.findEngine()); - (moveaction).safety_distance = 30; - battle.ship_separation = 30; + result = simulator.scanCircle(50, 30, 10, 3, 4); + check.equals(imaterialize(result), [ + new Target(50, 30), + new Target(55, 30), + new Target(45, 30), + new Target(60, 30), + new Target(50, 40), + new Target(40, 30), + new Target(50, 20) + ]); + }); - check.same(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); - check.same(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); - check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(400, 200)); + test.case("accounts for exclusion areas for the approach", check => { + let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50); + ship.setArenaPosition(300, 200); + let battle = new Battle(); + battle.fleets[0].addShip(ship); + let ship1 = battle.fleets[0].addShip(); + let moveaction = nn(simulator.findEngine()); + (moveaction).safety_distance = 30; + battle.ship_separation = 30; - ship1.setArenaPosition(420, 200); + check.same(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); + check.same(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED); + check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(400, 200)); - check.patch(simulator, "scanCircle", () => iarray([ - new Target(400, 200), - new Target(410, 200), - new Target(410, 230), - new Target(420, 210), - new Target(480, 260), - ])); - check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(410, 230)); - }); + ship1.setArenaPosition(420, 200); - test.case("moves to get in range, even if not enough AP to fire", check => { - let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5); - let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null)); - check.same(result.success, true, 'success'); - check.same(result.need_move, true, 'need_move'); - check.same(result.can_end_move, true, 'can_end_move'); - check.equals(result.move_location, new Target(ship.arena_x + 10, ship.arena_y, null)); - check.equals(result.total_move_ap, 2); - check.same(result.need_fire, true, 'need_fire'); - check.same(result.can_fire, false, 'can_fire'); - check.same(result.total_fire_ap, 2, 'total_fire_ap'); + check.patch(simulator, "scanCircle", () => iarray([ + new Target(400, 200), + new Target(410, 200), + new Target(410, 230), + new Target(420, 210), + new Target(480, 260), + ])); + check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(410, 230)); + }); - let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; - check.equals(result.parts, [ - { action: move_action, target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2, possible: true }, - { action: action, target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2, possible: false } - ]); - }); + test.case("moves to get in range, even if not enough AP to fire", check => { + let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null)); + check.same(result.success, true, 'success'); + check.same(result.need_move, true, 'need_move'); + check.same(result.can_end_move, true, 'can_end_move'); + check.equals(result.move_location, new Target(ship.arena_x + 10, ship.arena_y, null)); + check.equals(result.total_move_ap, 2); + check.same(result.need_fire, true, 'need_fire'); + check.same(result.can_fire, false, 'can_fire'); + check.same(result.total_fire_ap, 2, 'total_fire_ap'); - test.case("does nothing if trying to move in the same spot", check => { - let [ship, simulator, action] = simpleWeaponCase(); - let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; - let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null)); - check.equals(result.success, false); - check.equals(result.need_move, false); - check.equals(result.need_fire, false); - check.equals(result.parts, []); - }); + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; + check.equals(result.parts, [ + { action: move_action, target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2, possible: true }, + { action: action, target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2, possible: false } + ]); + }); - test.case("does not move if already in range, even if in the safety margin", check => { - let [ship, simulator, action] = simpleWeaponCase(100); - let result = simulator.simulateAction(action, new Target(ship.arena_x + 97, ship.arena_y, null), 5); - check.equals(result.success, true); - check.equals(result.need_move, false); - result = simulator.simulateAction(action, new Target(ship.arena_x + 101, ship.arena_y, null), 5); - check.equals(result.success, true); - check.equals(result.need_move, true); - check.equals(result.move_location, new Target(ship.arena_x + 6, ship.arena_y)); - }); + test.case("does nothing if trying to move in the same spot", check => { + let [ship, simulator, action] = simpleWeaponCase(); + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; + let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null)); + check.equals(result.success, false); + check.equals(result.need_move, false); + check.equals(result.need_fire, false); + check.equals(result.parts, []); + }); - test.case("simulates the results on a fake battle, to provide a list of expected diffs", check => { - let battle = TestTools.createBattle(); - let ship = battle.fleets[0].ships[0]; - let enemy = battle.fleets[1].ships[0]; - ship.setArenaPosition(100, 100); - enemy.setArenaPosition(300, 100); - TestTools.setShipModel(ship, 1, 1, 3); - TestTools.setShipModel(enemy, 2, 1); - let engine = TestTools.addEngine(ship, 80); - let weapon = TestTools.addWeapon(ship, 5, 1, 150); - let simulator = new MoveFireSimulator(ship); - let result = simulator.simulateAction(weapon, Target.newFromShip(enemy), 5); - let diffs = simulator.getExpectedDiffs(nn(ship.getBattle()), result); - check.equals(diffs, [ - new ShipActionUsedDiff(ship, engine, Target.newFromLocation(155, 100)), - new ShipValueDiff(ship, "power", -1), - new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(155, 100), engine), - new ShipActionUsedDiff(ship, weapon, Target.newFromShip(enemy)), - new ShipValueDiff(ship, "power", -1), - new ProjectileFiredDiff(ship, weapon, Target.newFromShip(enemy)), - new ShipDamageDiff(enemy, 2, 1, 0, 5), - new ShipValueDiff(enemy, "shield", -1), - new ShipValueDiff(enemy, "hull", -2), - new ShipDeathDiff(battle, enemy), - new EndBattleDiff(battle.fleets[0], 0) - ]); + test.case("does not move if already in range, even if in the safety margin", check => { + let [ship, simulator, action] = simpleWeaponCase(100); + let result = simulator.simulateAction(action, new Target(ship.arena_x + 97, ship.arena_y, null), 5); + check.equals(result.success, true); + check.equals(result.need_move, false); + result = simulator.simulateAction(action, new Target(ship.arena_x + 101, ship.arena_y, null), 5); + check.equals(result.success, true); + check.equals(result.need_move, true); + check.equals(result.move_location, new Target(ship.arena_x + 6, ship.arena_y)); + }); - check.equals(enemy.getValue("hull"), 2); - check.equals(enemy.getValue("hull"), 2); - }); - }); -} + test.case("simulates the results on a fake battle, to provide a list of expected diffs", check => { + let battle = TestTools.createBattle(); + let ship = battle.fleets[0].ships[0]; + let enemy = battle.fleets[1].ships[0]; + ship.setArenaPosition(100, 100); + enemy.setArenaPosition(300, 100); + TestTools.setShipModel(ship, 1, 1, 3); + TestTools.setShipModel(enemy, 2, 1); + let engine = TestTools.addEngine(ship, 80); + let weapon = TestTools.addWeapon(ship, 5, 1, 150); + let simulator = new MoveFireSimulator(ship); + let result = simulator.simulateAction(weapon, Target.newFromShip(enemy), 5); + let diffs = simulator.getExpectedDiffs(nn(ship.getBattle()), result); + check.equals(diffs, [ + new ShipActionUsedDiff(ship, engine, Target.newFromLocation(155, 100)), + new ShipValueDiff(ship, "power", -1), + new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(155, 100), engine), + new ShipActionUsedDiff(ship, weapon, Target.newFromShip(enemy)), + new ShipValueDiff(ship, "power", -1), + new ProjectileFiredDiff(ship, weapon, Target.newFromShip(enemy)), + new ShipDamageDiff(enemy, 2, 1, 0, 5), + new ShipValueDiff(enemy, "shield", -1), + new ShipValueDiff(enemy, "hull", -2), + new ShipDeathDiff(battle, enemy), + new EndBattleDiff(battle.fleets[0], 0) + ]); + + check.equals(enemy.getValue("hull"), 2); + check.equals(enemy.getValue("hull"), 2); + }); +}); diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index 81a8ec4..3a69d05 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -1,211 +1,220 @@ -module TK.SpaceTac { - /** - * Error codes for approach simulation - */ - export enum ApproachSimulationError { - NO_MOVE_NEEDED, - NO_VECTOR_FOUND, - } +import { NAMESPACE } from ".." +import { ichainit, iforeach, imap, irepeat, istep } from "../common/Iterators" +import { cfilter, duplicate, first, minBy, nn } from "../common/Tools" +import { BaseAction } from "./actions/BaseAction" +import { MoveAction } from "./actions/MoveAction" +import { arenaDistance } from "./ArenaLocation" +import { Battle } from "./Battle" +import { BaseBattleDiff } from "./diffs/BaseBattleDiff" +import { Ship } from "./Ship" +import { Target } from "./Target" - /** - * A single action in the sequence result from the simulator - */ - export type MoveFirePart = { - action: BaseAction - target: Target - ap: number - possible: boolean - } - - /** - * A simulation result - */ - export class MoveFireResult { - // Simulation success, false only if no route can be found - success = false - // Ideal successive parts to make the full move+fire - parts: MoveFirePart[] = [] - // Simulation complete (both move and fire are possible) - complete = false - - need_move = false - can_move = false - can_end_move = false - total_move_ap = 0 - move_location = new Target(0, 0, null) - - need_fire = false - can_fire = false - total_fire_ap = 0 - fire_location = new Target(0, 0, null) - }; - - /** - * Utility to simulate a move+fire action. - * - * This is also a helper to bring a ship in range to fire a weapon. - */ - export class MoveFireSimulator { - ship: Ship; - - constructor(ship: Ship) { - this.ship = ship; - } - - /** - * Find an engine action, to make an approach - * - * This will return the first available engine, in the definition order - */ - findEngine(): MoveAction | null { - let actions = cfilter(this.ship.actions.listAll(), MoveAction); - return first(actions, action => this.ship.actions.getCooldown(action).canUse()); - } - - /** - * Check that a move action can reach a given destination - */ - canMoveTo(action: MoveAction, target: Target): boolean { - let checked = action.checkLocationTarget(this.ship, target); - return checked != null && checked.x == target.x && checked.y == target.y; - } - - /** - * Get an iterator for scanning a circle - */ - scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterable { - let rcount = nr ? 1 / (nr - 1) : 0; - return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => { - let angles = Math.max(1, Math.ceil(na * r)); - return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => { - return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a)) - }); - })); - } - - /** - * Find the best approach location, to put a target in a given range. - * - * Return null if no approach vector was found. - */ - getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError { - let dx = target.x - this.ship.arena_x; - let dy = target.y - this.ship.arena_y; - let distance = Math.sqrt(dx * dx + dy * dy); - - if (distance <= radius) { - return ApproachSimulationError.NO_MOVE_NEEDED; - } else { - if (margin && radius > margin) { - radius -= margin; - } - let factor = (distance - radius) / distance; - let candidate = new Target(this.ship.arena_x + dx * factor, this.ship.arena_y + dy * factor); - if (this.canMoveTo(action, candidate)) { - return candidate; - } else { - let candidates: [number, Target][] = []; - iforeach(this.scanCircle(target.x, target.y, radius), candidate => { - if (this.canMoveTo(action, candidate)) { - candidates.push([candidate.getDistanceTo(this.ship.location), candidate]); - } - }); - - if (candidates.length) { - return minBy(candidates, ([distance, candidate]) => distance)[1]; - } else { - return ApproachSimulationError.NO_VECTOR_FOUND; - } - } - } - } - - /** - * Simulate a given action on a given valid target. - */ - simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult { - let result = new MoveFireResult(); - let ap = this.ship.getValue("power"); - - // Move or approach needed ? - let move_target: Target | null = null; - let move_action: MoveAction | null = null; - result.move_location = Target.newFromShip(this.ship); - if (action instanceof MoveAction) { - let corrected_target = action.applyReachableRange(this.ship, target, move_margin); - corrected_target = action.applyExclusion(this.ship, corrected_target); - if (corrected_target) { - result.need_move = target.getDistanceTo(this.ship.location) > 0; - move_target = corrected_target; - } - move_action = action; - } else { - move_action = this.findEngine(); - if (move_action) { - let approach_radius = action.getRangeRadius(this.ship); - let approach = this.getApproach(move_action, target, approach_radius, move_margin); - if (approach instanceof Target) { - result.need_move = true; - move_target = approach; - } else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) { - result.need_move = true; - result.can_move = false; - result.success = false; - return result; - } - } - } - if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) { - result.need_move = false; - } - - // Check move AP - if (result.need_move && move_target && move_action) { - result.total_move_ap = move_action.getPowerUsage(this.ship, move_target); - result.can_move = ap > 0; - result.can_end_move = result.total_move_ap <= ap; - result.move_location = move_target; - // TODO Split in "this turn" part and "next turn" part if needed - result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move }); - - ap -= result.total_move_ap; - } - - // Check action AP - if (action instanceof MoveAction) { - result.success = result.need_move && result.can_move; - } else { - result.need_fire = true; - result.total_fire_ap = action.getPowerUsage(this.ship, target); - result.can_fire = result.total_fire_ap <= ap; - result.fire_location = target; - result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire }); - result.success = true; - } - - result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire); - - return result; - } - - /** - * Apply a move-fire simulation result, and predict the diffs it will apply on a battle - * - * The original battle passed as parameter will be duplicated, and not altered - */ - getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] { - let sim_battle = duplicate(battle, TK.SpaceTac); - let sim_ship = nn(sim_battle.getShip(this.ship.id)); - let results: BaseBattleDiff[] = []; - simulation.parts.forEach(part => { - let diffs = part.action.getDiffs(sim_ship, battle, part.target); - results = results.concat(diffs); - sim_battle.applyDiffs(diffs); - - diffs = sim_battle.performChecks(); - results = results.concat(diffs); - }); - return results; - } - } +/** + * Error codes for approach simulation + */ +export enum ApproachSimulationError { + NO_MOVE_NEEDED, + NO_VECTOR_FOUND, +} + +/** + * A single action in the sequence result from the simulator + */ +export type MoveFirePart = { + action: BaseAction + target: Target + ap: number + possible: boolean +} + +/** + * A simulation result + */ +export class MoveFireResult { + // Simulation success, false only if no route can be found + success = false + // Ideal successive parts to make the full move+fire + parts: MoveFirePart[] = [] + // Simulation complete (both move and fire are possible) + complete = false + + need_move = false + can_move = false + can_end_move = false + total_move_ap = 0 + move_location = new Target(0, 0, null) + + need_fire = false + can_fire = false + total_fire_ap = 0 + fire_location = new Target(0, 0, null) +}; + +/** + * Utility to simulate a move+fire action. + * + * This is also a helper to bring a ship in range to fire a weapon. + */ +export class MoveFireSimulator { + ship: Ship; + + constructor(ship: Ship) { + this.ship = ship; + } + + /** + * Find an engine action, to make an approach + * + * This will return the first available engine, in the definition order + */ + findEngine(): MoveAction | null { + let actions = cfilter(this.ship.actions.listAll(), MoveAction); + return first(actions, action => this.ship.actions.getCooldown(action).canUse()); + } + + /** + * Check that a move action can reach a given destination + */ + canMoveTo(action: MoveAction, target: Target): boolean { + let checked = action.checkLocationTarget(this.ship, target); + return checked != null && checked.x == target.x && checked.y == target.y; + } + + /** + * Get an iterator for scanning a circle + */ + scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterable { + let rcount = nr ? 1 / (nr - 1) : 0; + return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => { + let angles = Math.max(1, Math.ceil(na * r)); + return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => { + return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a)) + }); + })); + } + + /** + * Find the best approach location, to put a target in a given range. + * + * Return null if no approach vector was found. + */ + getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError { + let dx = target.x - this.ship.arena_x; + let dy = target.y - this.ship.arena_y; + let distance = Math.sqrt(dx * dx + dy * dy); + + if (distance <= radius) { + return ApproachSimulationError.NO_MOVE_NEEDED; + } else { + if (margin && radius > margin) { + radius -= margin; + } + let factor = (distance - radius) / distance; + let candidate = new Target(this.ship.arena_x + dx * factor, this.ship.arena_y + dy * factor); + if (this.canMoveTo(action, candidate)) { + return candidate; + } else { + let candidates: [number, Target][] = []; + iforeach(this.scanCircle(target.x, target.y, radius), candidate => { + if (this.canMoveTo(action, candidate)) { + candidates.push([candidate.getDistanceTo(this.ship.location), candidate]); + } + }); + + if (candidates.length) { + return minBy(candidates, ([distance, candidate]) => distance)[1]; + } else { + return ApproachSimulationError.NO_VECTOR_FOUND; + } + } + } + } + + /** + * Simulate a given action on a given valid target. + */ + simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult { + let result = new MoveFireResult(); + let ap = this.ship.getValue("power"); + + // Move or approach needed ? + let move_target: Target | null = null; + let move_action: MoveAction | null = null; + result.move_location = Target.newFromShip(this.ship); + if (action instanceof MoveAction) { + let corrected_target = action.applyReachableRange(this.ship, target, move_margin); + corrected_target = action.applyExclusion(this.ship, corrected_target); + if (corrected_target) { + result.need_move = target.getDistanceTo(this.ship.location) > 0; + move_target = corrected_target; + } + move_action = action; + } else { + move_action = this.findEngine(); + if (move_action) { + let approach_radius = action.getRangeRadius(this.ship); + let approach = this.getApproach(move_action, target, approach_radius, move_margin); + if (approach instanceof Target) { + result.need_move = true; + move_target = approach; + } else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) { + result.need_move = true; + result.can_move = false; + result.success = false; + return result; + } + } + } + if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) { + result.need_move = false; + } + + // Check move AP + if (result.need_move && move_target && move_action) { + result.total_move_ap = move_action.getPowerUsage(this.ship, move_target); + result.can_move = ap > 0; + result.can_end_move = result.total_move_ap <= ap; + result.move_location = move_target; + // TODO Split in "this turn" part and "next turn" part if needed + result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move }); + + ap -= result.total_move_ap; + } + + // Check action AP + if (action instanceof MoveAction) { + result.success = result.need_move && result.can_move; + } else { + result.need_fire = true; + result.total_fire_ap = action.getPowerUsage(this.ship, target); + result.can_fire = result.total_fire_ap <= ap; + result.fire_location = target; + result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire }); + result.success = true; + } + + result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire); + + return result; + } + + /** + * Apply a move-fire simulation result, and predict the diffs it will apply on a battle + * + * The original battle passed as parameter will be duplicated, and not altered + */ + getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] { + let sim_battle = duplicate(battle, NAMESPACE); + let sim_ship = nn(sim_battle.getShip(this.ship.id)); + let results: BaseBattleDiff[] = []; + simulation.parts.forEach(part => { + let diffs = part.action.getDiffs(sim_ship, battle, part.target); + results = results.concat(diffs); + sim_battle.applyDiffs(diffs); + + diffs = sim_battle.performChecks(); + results = results.concat(diffs); + }); + return results; + } } diff --git a/src/core/NameGenerator.spec.ts b/src/core/NameGenerator.spec.ts index 906460f..e815762 100644 --- a/src/core/NameGenerator.spec.ts +++ b/src/core/NameGenerator.spec.ts @@ -1,13 +1,15 @@ -module TK.SpaceTac.Specs { - testing("NameGenerator", test => { - test.case("generates unique names", check => { - var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]); - var gen = new NameGenerator(["a", "b", "c"], random); +import { SkewedRandomGenerator } from "../common/RandomGenerator"; +import { testing } from "../common/Testing"; +import { NameGenerator } from "./NameGenerator"; - check.equals(gen.getName(), "b"); - check.equals(gen.getName(), "c"); - check.equals(gen.getName(), "a"); - check.equals(gen.getName(), null); - }); - }); -} +testing("NameGenerator", test => { + test.case("generates unique names", check => { + var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]); + var gen = new NameGenerator(["a", "b", "c"], random); + + check.equals(gen.getName(), "b"); + check.equals(gen.getName(), "c"); + check.equals(gen.getName(), "a"); + check.equals(gen.getName(), null); + }); +}); diff --git a/src/core/NameGenerator.ts b/src/core/NameGenerator.ts index 45db198..e35341a 100644 --- a/src/core/NameGenerator.ts +++ b/src/core/NameGenerator.ts @@ -1,27 +1,28 @@ -module TK.SpaceTac { - // A unique name generator - export class NameGenerator { - // List of available choices - private choices: string[]; +import { RandomGenerator } from "../common/RandomGenerator"; +import { acopy } from "../common/Tools"; - // Random generator to use - private random: RandomGenerator; +// A unique name generator +export class NameGenerator { + // List of available choices + private choices: string[]; - constructor(choices: string[], random: RandomGenerator = new RandomGenerator()) { - this.choices = acopy(choices); - this.random = random; - } + // Random generator to use + private random: RandomGenerator; - // Get a new unique name from available choices - getName(): string | null { - if (this.choices.length === 0) { - return null; - } + constructor(choices: string[], random: RandomGenerator = new RandomGenerator()) { + this.choices = acopy(choices); + this.random = random; + } - var index = this.random.randInt(0, this.choices.length - 1); - var result = this.choices[index]; - this.choices.splice(index, 1); - return result; - } + // Get a new unique name from available choices + getName(): string | null { + if (this.choices.length === 0) { + return null; } + + var index = this.random.randInt(0, this.choices.length - 1); + var result = this.choices[index]; + this.choices.splice(index, 1); + return result; + } } diff --git a/src/core/Personality.ts b/src/core/Personality.ts index 1fb1f75..53deb8c 100644 --- a/src/core/Personality.ts +++ b/src/core/Personality.ts @@ -1,36 +1,34 @@ -module TK.SpaceTac { - /** - * List of personality traits (may be used with "keyof"). - */ - export interface IPersonalityTraits { - aggressive: number - funny: number - heroic: number - optimistic: number - } - - /** - * A personality is a set of traits that defines how a character thinks and behaves - * - * Each trait is a number between -1 and 1 - * - * In the game, a personality represents an artificial intelligence, and is transferable - * from one ship (body) to another. This is why a personality has a name - */ - export class Personality implements IPersonalityTraits { - // Name of this personality - name = "" - - // Aggressive 1 / Poised -1 - aggressive = 0 - - // Funny 1 / Serious -1 - funny = 0 - - // Heroic 1 / Coward -1 - heroic = 0 - - // Optimistic 1 / Pessimistic -1 - optimistic = 0 - } +/** + * List of personality traits (may be used with "keyof"). + */ +export interface IPersonalityTraits { + aggressive: number + funny: number + heroic: number + optimistic: number +} + +/** + * A personality is a set of traits that defines how a character thinks and behaves + * + * Each trait is a number between -1 and 1 + * + * In the game, a personality represents an artificial intelligence, and is transferable + * from one ship (body) to another. This is why a personality has a name + */ +export class Personality implements IPersonalityTraits { + // Name of this personality + name = "" + + // Aggressive 1 / Poised -1 + aggressive = 0 + + // Funny 1 / Serious -1 + funny = 0 + + // Heroic 1 / Coward -1 + heroic = 0 + + // Optimistic 1 / Pessimistic -1 + optimistic = 0 } diff --git a/src/core/PersonalityReactions.spec.ts b/src/core/PersonalityReactions.spec.ts index d282f92..48eb25a 100644 --- a/src/core/PersonalityReactions.spec.ts +++ b/src/core/PersonalityReactions.spec.ts @@ -1,65 +1,70 @@ -module TK.SpaceTac.Specs { - testing("PersonalityReactions", test => { - function apply(pool: ReactionPool): PersonalityReaction | null { - let reactions = new PersonalityReactions(); - return reactions.check(new Player(), null, null, null, pool); - } +import { testing } from "../common/Testing"; +import { Battle } from "./Battle"; +import { ShipDamageDiff } from "./diffs/ShipDamageDiff"; +import { BUILTIN_REACTION_POOL, PersonalityReaction, PersonalityReactionConversation, PersonalityReactions, ReactionPool } from "./PersonalityReactions"; +import { Player } from "./Player"; +import { Ship } from "./Ship"; - class FakeReaction extends PersonalityReactionConversation { - ships: Ship[] - constructor(ships: Ship[]) { - super([]); - this.ships = ships; - } - static cons(ships: Ship[]): FakeReaction { - return new FakeReaction(ships); - } - } +testing("PersonalityReactions", test => { + function apply(pool: ReactionPool): PersonalityReaction | null { + let reactions = new PersonalityReactions(); + return reactions.check(new Player(), null, null, null, pool); + } - test.case("fetches ships from conditions", check => { - let reaction = apply({}); - check.equals(reaction, null); + class FakeReaction extends PersonalityReactionConversation { + ships: Ship[] + constructor(ships: Ship[]) { + super([]); + this.ships = ships; + } + static cons(ships: Ship[]): FakeReaction { + return new FakeReaction(ships); + } + } - let s1 = new Ship(null, "S1"); - let s2 = new Ship(null, "S2"); + test.case("fetches ships from conditions", check => { + let reaction = apply({}); + check.equals(reaction, null); - reaction = apply({ - a: [() => [s1, s2], 1, [[() => 1, FakeReaction.cons]]], - }); - check.equals(reaction, new FakeReaction([s1, s2])); - }) + let s1 = new Ship(null, "S1"); + let s2 = new Ship(null, "S2"); - test.case("applies weight on conditions", check => { - let s1 = new Ship(null, "S1"); - let s2 = new Ship(null, "S2"); + reaction = apply({ + a: [() => [s1, s2], 1, [[() => 1, FakeReaction.cons]]], + }); + check.equals(reaction, new FakeReaction([s1, s2])); + }) - let reaction = apply({ - a: [() => [s1], 1, [[() => 1, FakeReaction.cons]]], - b: [() => [s2], 0, [[() => 1, FakeReaction.cons]]], - }); - check.equals(reaction, new FakeReaction([s1])); + test.case("applies weight on conditions", check => { + let s1 = new Ship(null, "S1"); + let s2 = new Ship(null, "S2"); - reaction = apply({ - a: [() => [s1], 0, [[() => 1, FakeReaction.cons]]], - b: [() => [s2], 1, [[() => 1, FakeReaction.cons]]], - }); - check.equals(reaction, new FakeReaction([s2])); - }) + let reaction = apply({ + a: [() => [s1], 1, [[() => 1, FakeReaction.cons]]], + b: [() => [s2], 0, [[() => 1, FakeReaction.cons]]], + }); + check.equals(reaction, new FakeReaction([s1])); - test.case("checks for friendly fire", check => { - let condition = BUILTIN_REACTION_POOL['friendly_fire'][0]; - let battle = new Battle(); - let player = new Player(); - battle.fleets[0].setPlayer(player); - let ship1a = battle.fleets[0].addShip(); - let ship1b = battle.fleets[0].addShip(); - let ship2a = battle.fleets[1].addShip(); - let ship2b = battle.fleets[1].addShip(); + reaction = apply({ + a: [() => [s1], 0, [[() => 1, FakeReaction.cons]]], + b: [() => [s2], 1, [[() => 1, FakeReaction.cons]]], + }); + check.equals(reaction, new FakeReaction([s2])); + }) - check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1a, 50, 10)), [], "self shoot"); - check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1b, 50, 10)), [ship1b, ship1a]); - check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot"); - check.equals(condition(player, battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event"); - }) - }) -} + test.case("checks for friendly fire", check => { + let condition = BUILTIN_REACTION_POOL['friendly_fire'][0]; + let battle = new Battle(); + let player = new Player(); + battle.fleets[0].setPlayer(player); + let ship1a = battle.fleets[0].addShip(); + let ship1b = battle.fleets[0].addShip(); + let ship2a = battle.fleets[1].addShip(); + let ship2b = battle.fleets[1].addShip(); + + check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1a, 50, 10)), [], "self shoot"); + check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1b, 50, 10)), [ship1b, ship1a]); + check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot"); + check.equals(condition(player, battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event"); + }) +}) diff --git a/src/core/PersonalityReactions.ts b/src/core/PersonalityReactions.ts index d260310..90fa229 100644 --- a/src/core/PersonalityReactions.ts +++ b/src/core/PersonalityReactions.ts @@ -1,106 +1,113 @@ -module TK.SpaceTac { - // Reaction triggered - export type PersonalityReaction = PersonalityReactionConversation +import { RandomGenerator } from "../common/RandomGenerator" +import { difference, keys, nna } from "../common/Tools" +import { Battle } from "./Battle" +import { BaseBattleDiff } from "./diffs/BaseBattleDiff" +import { ShipDamageDiff } from "./diffs/ShipDamageDiff" +import { IPersonalityTraits } from "./Personality" +import { Player } from "./Player" +import { Ship } from "./Ship" - // Condition to check if a reaction may happen, returning involved ships (order is important) - export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null) => Ship[] +// Reaction triggered +export type PersonalityReaction = PersonalityReactionConversation - // Reaction profile, giving a probability for types of personality, and an associated reaction constructor - export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction] +// Condition to check if a reaction may happen, returning involved ships (order is important) +export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null) => Ship[] - // Reaction config (condition, chance, profiles) - export type ReactionConfig = [ReactionCondition, number, ReactionProfile[]] +// Reaction profile, giving a probability for types of personality, and an associated reaction constructor +export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction] - // Pool of reaction config - export type ReactionPool = { [code: string]: ReactionConfig } +// Reaction config (condition, chance, profiles) +export type ReactionConfig = [ReactionCondition, number, ReactionProfile[]] - /** - * Reactions to external events according to personalities. - * - * This allows for a more "alive" world, as characters tend to speak to react to events. - * - * This object will store the previous reactions to avoid too much recurrence, and should be global to a whole - * game session. - */ - export class PersonalityReactions { - done: string[] = [] - random = RandomGenerator.global +// Pool of reaction config +export type ReactionPool = { [code: string]: ReactionConfig } - /** - * Check for a reaction. - * - * This will return a reaction to display, and add it to the done list - */ - check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleDiff | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null { - let codes = difference(keys(pool), this.done); +/** + * Reactions to external events according to personalities. + * + * This allows for a more "alive" world, as characters tend to speak to react to events. + * + * This object will store the previous reactions to avoid too much recurrence, and should be global to a whole + * game session. + */ +export class PersonalityReactions { + done: string[] = [] + random = RandomGenerator.global - let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => { - let [condition, chance, profiles] = pool[code]; - if (this.random.random() <= chance) { - let involved = condition(player, battle, ship, event); - if (involved.length > 0) { - return [code, involved, profiles]; - } else { - return null; - } - } else { - return null; - } - })); + /** + * Check for a reaction. + * + * This will return a reaction to display, and add it to the done list + */ + check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleDiff | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null { + let codes = difference(keys(pool), this.done); - if (candidates.length > 0) { - let [code, involved, profiles] = this.random.choice(candidates); - let primary = involved[0]; - let weights = profiles.map(([evaluator, _]) => evaluator(primary.personality)); - let action_number = this.random.weighted(weights); - if (action_number >= 0) { - this.done.push(code); - let reaction_constructor = profiles[action_number][1]; - return reaction_constructor(involved); - } else { - return null; - } - } else { - return null; - } - } - } - - /** - * One kind of personality reaction: saying something out loud - */ - export class PersonalityReactionConversation { - messages: { interlocutor: Ship, message: string }[] - constructor(messages: { interlocutor: Ship, message: string }[]) { - this.messages = messages; - } - } - - /** - * Standard reaction pool - */ - export const BUILTIN_REACTION_POOL: ReactionPool = { - friendly_fire: [cond_friendly_fire, 1, [ - [traits => 1, ships => new PersonalityReactionConversation([ - { interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" }, - { interlocutor: ships[1], message: "Sorry mate..." }, - ])] - ]] - } - - /** - * Check for a friendly fire condition (one of player's ships fired on another) - */ - function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] { - if (battle && ship && event) { - if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) { - let hurt = battle.getShip(event.ship_id); - return (hurt && player.is(hurt.fleet.player)) ? [hurt, ship] : []; - } else { - return []; - } + let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => { + let [condition, chance, profiles] = pool[code]; + if (this.random.random() <= chance) { + let involved = condition(player, battle, ship, event); + if (involved.length > 0) { + return [code, involved, profiles]; } else { - return []; + return null; } + } else { + return null; + } + })); + + if (candidates.length > 0) { + let [code, involved, profiles] = this.random.choice(candidates); + let primary = involved[0]; + let weights = profiles.map(([evaluator, _]) => evaluator(primary.personality)); + let action_number = this.random.weighted(weights); + if (action_number >= 0) { + this.done.push(code); + let reaction_constructor = profiles[action_number][1]; + return reaction_constructor(involved); + } else { + return null; + } + } else { + return null; } + } +} + +/** + * One kind of personality reaction: saying something out loud + */ +export class PersonalityReactionConversation { + messages: { interlocutor: Ship, message: string }[] + constructor(messages: { interlocutor: Ship, message: string }[]) { + this.messages = messages; + } +} + +/** + * Standard reaction pool + */ +export const BUILTIN_REACTION_POOL: ReactionPool = { + friendly_fire: [cond_friendly_fire, 1, [ + [traits => 1, ships => new PersonalityReactionConversation([ + { interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" }, + { interlocutor: ships[1], message: "Sorry mate..." }, + ])] + ]] +} + +/** + * Check for a friendly fire condition (one of player's ships fired on another) + */ +function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] { + if (battle && ship && event) { + if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) { + let hurt = battle.getShip(event.ship_id); + return (hurt && player.is(hurt.fleet.player)) ? [hurt, ship] : []; + } else { + return []; + } + } else { + return []; + } } diff --git a/src/core/Player.spec.ts b/src/core/Player.spec.ts index 7ebd5c2..abd892a 100644 --- a/src/core/Player.spec.ts +++ b/src/core/Player.spec.ts @@ -1,38 +1,41 @@ -module TK.SpaceTac { - testing("Player", test => { - test.case("keeps track of visited locations", check => { - let player = new Player(); - let universe = new Universe(); - let star1 = universe.addStar(); - let star2 = universe.addStar(); - let loc1a = star1.addLocation(StarLocationType.PLANET); - let loc1b = star1.addLocation(StarLocationType.PLANET); - let loc2a = star2.addLocation(StarLocationType.PLANET); - let loc2b = star2.addLocation(StarLocationType.PLANET); - universe.updateLocations(); +import { testing } from "../common/Testing"; +import { Player } from "./Player"; +import { StarLocationType } from "./StarLocation"; +import { Universe } from "./Universe"; - function checkVisited(s1 = false, s2 = false, v1a = false, v1b = false, v2a = false, v2b = false) { - check.same(player.hasVisitedSystem(star1), s1); - check.same(player.hasVisitedSystem(star2), s2); - check.same(player.hasVisitedLocation(loc1a), v1a); - check.same(player.hasVisitedLocation(loc1b), v1b); - check.same(player.hasVisitedLocation(loc2a), v2a); - check.same(player.hasVisitedLocation(loc2b), v2b); - } +testing("Player", test => { + test.case("keeps track of visited locations", check => { + let player = new Player(); + let universe = new Universe(); + let star1 = universe.addStar(); + let star2 = universe.addStar(); + let loc1a = star1.addLocation(StarLocationType.PLANET); + let loc1b = star1.addLocation(StarLocationType.PLANET); + let loc2a = star2.addLocation(StarLocationType.PLANET); + let loc2b = star2.addLocation(StarLocationType.PLANET); + universe.updateLocations(); - checkVisited(); + function checkVisited(s1 = false, s2 = false, v1a = false, v1b = false, v2a = false, v2b = false) { + check.same(player.hasVisitedSystem(star1), s1); + check.same(player.hasVisitedSystem(star2), s2); + check.same(player.hasVisitedLocation(loc1a), v1a); + check.same(player.hasVisitedLocation(loc1b), v1b); + check.same(player.hasVisitedLocation(loc2a), v2a); + check.same(player.hasVisitedLocation(loc2b), v2b); + } - player.fleet.setLocation(loc1b); - checkVisited(true, false, false, true, false, false); + checkVisited(); - player.fleet.setLocation(loc1a); - checkVisited(true, false, true, true, false, false); + player.fleet.setLocation(loc1b); + checkVisited(true, false, false, true, false, false); - player.fleet.setLocation(loc2a); - checkVisited(true, true, true, true, true, false); + player.fleet.setLocation(loc1a); + checkVisited(true, false, true, true, false, false); - player.fleet.setLocation(loc2a); - checkVisited(true, true, true, true, true, false); - }); - }); -} \ No newline at end of file + player.fleet.setLocation(loc2a); + checkVisited(true, true, true, true, true, false); + + player.fleet.setLocation(loc2a); + checkVisited(true, true, true, true, true, false); + }); +}); diff --git a/src/core/Player.ts b/src/core/Player.ts index 821e107..14ec633 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -1,78 +1,84 @@ -/// +import { RObject } from "../common/RObject"; +import { contains, intersection } from "../common/Tools"; +import { Battle } from "./Battle"; +import { BattleCheats } from "./BattleCheats"; +import { Fleet } from "./Fleet"; +import { FleetGenerator } from "./FleetGenerator"; +import { ActiveMissions } from "./missions/ActiveMissions"; +import { Star } from "./Star"; +import { StarLocation } from "./StarLocation"; -module TK.SpaceTac { - /** - * One player (human or IA) - */ - export class Player extends RObject { - // Player's name - name: string +/** + * One player (human or IA) + */ +export class Player extends RObject { + // Player's name + name: string - // Bound fleet - fleet: Fleet + // Bound fleet + fleet: Fleet - // Active missions - missions = new ActiveMissions() + // Active missions + missions = new ActiveMissions() - // Create a player, with an empty fleet - constructor(name = "Player", fleet?: Fleet) { - super(); + // Create a player, with an empty fleet + constructor(name = "Player", fleet?: Fleet) { + super(); - this.name = name; - this.fleet = fleet || new Fleet(this); + this.name = name; + this.fleet = fleet || new Fleet(this); - this.fleet.setPlayer(this); - } + this.fleet.setPlayer(this); + } - // Create a quick random player, with a fleet, for testing purposes - static newQuickRandom(name: string, level = 1, shipcount = 4, upgrade = false): Player { - let player = new Player(name); - let generator = new FleetGenerator(); - player.fleet = generator.generate(level, player, shipcount, upgrade); - return player; - } + // Create a quick random player, with a fleet, for testing purposes + static newQuickRandom(name: string, level = 1, shipcount = 4, upgrade = false): Player { + let player = new Player(name); + let generator = new FleetGenerator(); + player.fleet = generator.generate(level, player, shipcount, upgrade); + return player; + } - /** - * Set the fleet for this player - */ - setFleet(fleet: Fleet): void { - this.fleet = fleet; - fleet.setPlayer(this); - } + /** + * Set the fleet for this player + */ + setFleet(fleet: Fleet): void { + this.fleet = fleet; + fleet.setPlayer(this); + } - /** - * Get a cheats object - */ - getCheats(): BattleCheats | null { - let battle = this.getBattle(); - if (battle) { - return new BattleCheats(battle, this); - } else { - return null; - } - } - - /** - * Return true if the player has visited at least one location in a given system. - */ - hasVisitedSystem(system: Star): boolean { - return intersection(this.fleet.visited, system.locations.map(loc => loc.id)).length > 0; - } - - /** - * Return true if the player has visited a given star location. - */ - hasVisitedLocation(location: StarLocation): boolean { - return contains(this.fleet.visited, location.id); - } - - // Get currently played battle, null when none is in progress - getBattle(): Battle | null { - return this.fleet.battle; - } - setBattle(battle: Battle | null): void { - this.fleet.setBattle(battle); - this.missions.checkStatus(); - } + /** + * Get a cheats object + */ + getCheats(): BattleCheats | null { + let battle = this.getBattle(); + if (battle) { + return new BattleCheats(battle, this); + } else { + return null; } + } + + /** + * Return true if the player has visited at least one location in a given system. + */ + hasVisitedSystem(system: Star): boolean { + return intersection(this.fleet.visited, system.locations.map(loc => loc.id)).length > 0; + } + + /** + * Return true if the player has visited a given star location. + */ + hasVisitedLocation(location: StarLocation): boolean { + return contains(this.fleet.visited, location.id); + } + + // Get currently played battle, null when none is in progress + getBattle(): Battle | null { + return this.fleet.battle; + } + setBattle(battle: Battle | null): void { + this.fleet.setBattle(battle); + this.missions.checkStatus(); + } } diff --git a/src/core/Range.spec.ts b/src/core/Range.spec.ts index be440f0..e1d3805 100644 --- a/src/core/Range.spec.ts +++ b/src/core/Range.spec.ts @@ -1,45 +1,46 @@ -module TK.SpaceTac.Specs { - testing("Range", test => { - test.case("can work with proportional values", check => { - var range = new Range(1, 5); +import { testing } from "../common/Testing"; +import { IntegerRange } from "./Range"; - function checkProportional(range: Range, value1: number, value2: number) { - check.equals(range.getProportional(value1), value2); - check.equals(range.getReverseProportional(value2), value1); - } +testing("Range", test => { + test.case("can work with proportional values", check => { + var range = new Range(1, 5); - checkProportional(range, 0, 1); - checkProportional(range, 1, 5); - checkProportional(range, 0.5, 3); - checkProportional(range, 0.4, 2.6); + function checkProportional(range: Range, value1: number, value2: number) { + check.equals(range.getProportional(value1), value2); + check.equals(range.getReverseProportional(value2), value1); + } - check.equals(range.getProportional(-0.25), 1); - check.equals(range.getProportional(1.8), 5); + checkProportional(range, 0, 1); + checkProportional(range, 1, 5); + checkProportional(range, 0.5, 3); + checkProportional(range, 0.4, 2.6); - check.equals(range.getReverseProportional(0), 0); - check.equals(range.getReverseProportional(6), 1); - }); - }); + check.equals(range.getProportional(-0.25), 1); + check.equals(range.getProportional(1.8), 5); - testing("IntegerRange", test => { - test.case("can work with proportional values", check => { - var range = new IntegerRange(1, 5); + check.equals(range.getReverseProportional(0), 0); + check.equals(range.getReverseProportional(6), 1); + }); +}); - check.equals(range.getProportional(0), 1); - check.equals(range.getProportional(0.1), 1); - check.equals(range.getProportional(0.2), 2); - check.equals(range.getProportional(0.45), 3); - check.equals(range.getProportional(0.5), 3); - check.equals(range.getProportional(0.75), 4); - check.equals(range.getProportional(0.8), 5); - check.equals(range.getProportional(0.99), 5); - check.equals(range.getProportional(1), 5); +testing("IntegerRange", test => { + test.case("can work with proportional values", check => { + var range = new IntegerRange(1, 5); - check.equals(range.getReverseProportional(1), 0); - check.equals(range.getReverseProportional(2), 0.2); - check.equals(range.getReverseProportional(3), 0.4); - check.equals(range.getReverseProportional(4), 0.6); - check.equals(range.getReverseProportional(5), 0.8); - }); - }); -} + check.equals(range.getProportional(0), 1); + check.equals(range.getProportional(0.1), 1); + check.equals(range.getProportional(0.2), 2); + check.equals(range.getProportional(0.45), 3); + check.equals(range.getProportional(0.5), 3); + check.equals(range.getProportional(0.75), 4); + check.equals(range.getProportional(0.8), 5); + check.equals(range.getProportional(0.99), 5); + check.equals(range.getProportional(1), 5); + + check.equals(range.getReverseProportional(1), 0); + check.equals(range.getReverseProportional(2), 0.2); + check.equals(range.getReverseProportional(3), 0.4); + check.equals(range.getReverseProportional(4), 0.6); + check.equals(range.getReverseProportional(5), 0.8); + }); +}); diff --git a/src/core/Range.ts b/src/core/Range.ts index 5d5e7fe..69daa0d 100644 --- a/src/core/Range.ts +++ b/src/core/Range.ts @@ -1,82 +1,80 @@ -module TK.SpaceTac { - // Range of number values - export class Range { - // Minimal value - min = 0 +// Range of number values +export class Range { + // Minimal value + min = 0 - // Maximal value - max = 0 + // Maximal value + max = 0 - // Create a range of values - constructor(min: number, max: number | null = null) { - this.set(min, max); - } + // Create a range of values + constructor(min: number, max: number | null = null) { + this.set(min, max); + } - // Change the range - set(min: number, max: number | null = null) { - this.min = min; - if (max === null) { - this.max = this.min; - } else { - this.max = max; - } - } - - // Get a proportional value (give 0.0-1.0 value to obtain a value in range) - getProportional(cursor: number): number { - if (cursor <= 0.0) { - return this.min; - } else if (cursor >= 1.0) { - return this.max; - } else { - return (this.max - this.min) * cursor + this.min; - } - } - - // Get the value of the cursor that would give this proportional value (in 0.0-1.0 range) - getReverseProportional(expected: number): number { - if (expected <= this.min) { - return 0; - } else if (expected >= this.max) { - return 1; - } else { - return (expected - this.min) / (this.max - this.min); - } - } - - // Check if a value is in the range - isInRange(value: number): boolean { - return value >= this.min && value <= this.max; - } + // Change the range + set(min: number, max: number | null = null) { + this.min = min; + if (max === null) { + this.max = this.min; + } else { + this.max = max; } + } - - // Range of integer values - // - // This differs from Range in that it adds space in proportional values to include the 'max'. - // Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999. - // This fixes this behavior. - // - // As this rounds values to integer, the 'reverse' proportional is no longer a bijection. - export class IntegerRange extends Range { - getProportional(cursor: number): number { - if (cursor <= 0.0) { - return this.min; - } else if (cursor >= 1.0) { - return this.max; - } else { - return Math.floor((this.max - this.min + 1) * cursor + this.min); - } - } - - getReverseProportional(expected: number): number { - if (expected <= this.min) { - return 0; - } else if (expected > this.max) { - return 1; - } else { - return (expected - this.min) * 1.0 / (this.max - this.min + 1); - } - } + // Get a proportional value (give 0.0-1.0 value to obtain a value in range) + getProportional(cursor: number): number { + if (cursor <= 0.0) { + return this.min; + } else if (cursor >= 1.0) { + return this.max; + } else { + return (this.max - this.min) * cursor + this.min; } + } + + // Get the value of the cursor that would give this proportional value (in 0.0-1.0 range) + getReverseProportional(expected: number): number { + if (expected <= this.min) { + return 0; + } else if (expected >= this.max) { + return 1; + } else { + return (expected - this.min) / (this.max - this.min); + } + } + + // Check if a value is in the range + isInRange(value: number): boolean { + return value >= this.min && value <= this.max; + } +} + + +// Range of integer values +// +// This differs from Range in that it adds space in proportional values to include the 'max'. +// Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999. +// This fixes this behavior. +// +// As this rounds values to integer, the 'reverse' proportional is no longer a bijection. +export class IntegerRange extends Range { + getProportional(cursor: number): number { + if (cursor <= 0.0) { + return this.min; + } else if (cursor >= 1.0) { + return this.max; + } else { + return Math.floor((this.max - this.min + 1) * cursor + this.min); + } + } + + getReverseProportional(expected: number): number { + if (expected <= this.min) { + return 0; + } else if (expected > this.max) { + return 1; + } else { + return (expected - this.min) * 1.0 / (this.max - this.min + 1); + } + } } diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index 6315976..174cbbd 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -1,195 +1,210 @@ -module TK.SpaceTac.Specs { - testing("Ship", test => { - test.case("creates a full name", check => { - let ship = new Ship(); - check.equals(ship.getName(false), "Ship"); - check.equals(ship.getName(true), "Level 1 Ship"); +import { testing } from "../common/Testing"; +import { nn } from "../common/Tools"; +import { BaseAction } from "./actions/BaseAction"; +import { ToggleAction } from "./actions/ToggleAction"; +import { Battle } from "./Battle"; +import { ShipActionToggleDiff } from "./diffs/ShipActionToggleDiff"; +import { ShipAttributeDiff } from "./diffs/ShipAttributeDiff"; +import { ShipDeathDiff } from "./diffs/ShipDeathDiff"; +import { ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff"; +import { ShipValueDiff } from "./diffs/ShipValueDiff"; +import { AttributeEffect } from "./effects/AttributeEffect"; +import { AttributeLimitEffect } from "./effects/AttributeLimitEffect"; +import { StickyEffect } from "./effects/StickyEffect"; +import { ShipModel } from "./models/ShipModel"; +import { Ship } from "./Ship"; +import { TestTools } from "./TestTools"; - ship.model = new ShipModel("test", "Hauler"); - check.equals(ship.getName(false), "Hauler"); - check.equals(ship.getName(true), "Level 1 Hauler"); +testing("Ship", test => { + test.case("creates a full name", check => { + let ship = new Ship(); + check.equals(ship.getName(false), "Ship"); + check.equals(ship.getName(true), "Level 1 Ship"); - ship.name = "Titan-W12"; - check.equals(ship.getName(false), "Titan-W12"); - check.equals(ship.getName(true), "Level 1 Titan-W12"); + ship.model = new ShipModel("test", "Hauler"); + check.equals(ship.getName(false), "Hauler"); + check.equals(ship.getName(true), "Level 1 Hauler"); - ship.level.forceLevel(3); - check.equals(ship.getName(false), "Titan-W12"); - check.equals(ship.getName(true), "Level 3 Titan-W12"); - }); + ship.name = "Titan-W12"; + check.equals(ship.getName(false), "Titan-W12"); + check.equals(ship.getName(true), "Level 1 Titan-W12"); - test.case("moves in the arena", check => { - let ship = new Ship(null, "Test"); - let engine = TestTools.addEngine(ship, 50); + ship.level.forceLevel(3); + check.equals(ship.getName(false), "Titan-W12"); + check.equals(ship.getName(true), "Level 3 Titan-W12"); + }); - check.equals(ship.arena_x, 0); - check.equals(ship.arena_y, 0); - check.equals(ship.arena_angle, 0); + test.case("moves in the arena", check => { + let ship = new Ship(null, "Test"); + let engine = TestTools.addEngine(ship, 50); - ship.setArenaFacingAngle(1.2); - ship.setArenaPosition(12, 50); + check.equals(ship.arena_x, 0); + check.equals(ship.arena_y, 0); + check.equals(ship.arena_angle, 0); - check.equals(ship.arena_x, 12); - check.equals(ship.arena_y, 50); - check.nears(ship.arena_angle, 1.2); - }); + ship.setArenaFacingAngle(1.2); + ship.setArenaPosition(12, 50); - test.case("applies permanent effects of ship model on attributes", check => { - let model = new ShipModel(); - let ship = new Ship(null, null, model); + check.equals(ship.arena_x, 12); + check.equals(ship.arena_y, 50); + check.nears(ship.arena_angle, 1.2); + }); - check.patch(model, "getEffects", () => [ - new AttributeEffect("power_capacity", 4), - new AttributeEffect("power_capacity", 5), - ]); + test.case("applies permanent effects of ship model on attributes", check => { + let model = new ShipModel(); + let ship = new Ship(null, null, model); - ship.updateAttributes(); - check.equals(ship.getAttribute("power_capacity"), 9); - }); + check.patch(model, "getEffects", () => [ + new AttributeEffect("power_capacity", 4), + new AttributeEffect("power_capacity", 5), + ]); - test.case("repairs hull and recharges shield", check => { - var ship = new Ship(null, "Test"); + ship.updateAttributes(); + check.equals(ship.getAttribute("power_capacity"), 9); + }); - TestTools.setAttribute(ship, "hull_capacity", 120); - TestTools.setAttribute(ship, "shield_capacity", 150); + test.case("repairs hull and recharges shield", check => { + var ship = new Ship(null, "Test"); - check.equals(ship.getValue("hull"), 0); - check.equals(ship.getValue("shield"), 0); + TestTools.setAttribute(ship, "hull_capacity", 120); + TestTools.setAttribute(ship, "shield_capacity", 150); - ship.restoreHealth(); + check.equals(ship.getValue("hull"), 0); + check.equals(ship.getValue("shield"), 0); - check.equals(ship.getValue("hull"), 120); - check.equals(ship.getValue("shield"), 150); - }); + ship.restoreHealth(); - test.case("checks if a ship is able to play", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - ship.setValue("hull", 10); + check.equals(ship.getValue("hull"), 120); + check.equals(ship.getValue("shield"), 150); + }); - check.equals(ship.isAbleToPlay(), false); - check.equals(ship.isAbleToPlay(false), true); + test.case("checks if a ship is able to play", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + ship.setValue("hull", 10); - ship.setValue("power", 5); + check.equals(ship.isAbleToPlay(), false); + check.equals(ship.isAbleToPlay(false), true); - check.equals(ship.isAbleToPlay(), true); - check.equals(ship.isAbleToPlay(false), true); + ship.setValue("power", 5); - ship.setDead(); + check.equals(ship.isAbleToPlay(), true); + check.equals(ship.isAbleToPlay(false), true); - check.equals(ship.isAbleToPlay(), false); - check.equals(ship.isAbleToPlay(false), false); - }); + ship.setDead(); - test.case("checks if a ship is inside a given circle", check => { - let ship = new Ship(); - ship.arena_x = 5; - ship.arena_y = 8; + check.equals(ship.isAbleToPlay(), false); + check.equals(ship.isAbleToPlay(false), false); + }); - check.equals(ship.isInCircle(5, 8, 0), true); - check.equals(ship.isInCircle(5, 8, 1), true); - check.equals(ship.isInCircle(5, 7, 1), true); - check.equals(ship.isInCircle(6, 9, 1.7), true); - check.equals(ship.isInCircle(5, 8.1, 0), false); - check.equals(ship.isInCircle(5, 7, 0.9), false); - check.equals(ship.isInCircle(12, -4, 5), false); - }); + test.case("checks if a ship is inside a given circle", check => { + let ship = new Ship(); + ship.arena_x = 5; + ship.arena_y = 8; - test.case("restores as new at the end of battle", check => { - let ship = new Ship(); - TestTools.setShipModel(ship, 10, 20, 5); - ship.setValue("hull", 5); - ship.setValue("shield", 15); - ship.setValue("power", 2); - ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12)); - ship.updateAttributes(); - let action1 = new BaseAction("action1"); - ship.actions.addCustom(action1); - let action2 = new ToggleAction("action2"); - ship.actions.addCustom(action2); - ship.actions.toggle(action2, true); - let action3 = new ToggleAction("action3"); - ship.actions.addCustom(action3); + check.equals(ship.isInCircle(5, 8, 0), true); + check.equals(ship.isInCircle(5, 8, 1), true); + check.equals(ship.isInCircle(5, 7, 1), true); + check.equals(ship.isInCircle(6, 9, 1.7), true); + check.equals(ship.isInCircle(5, 8.1, 0), false); + check.equals(ship.isInCircle(5, 7, 0.9), false); + check.equals(ship.isInCircle(12, -4, 5), false); + }); - check.in("before", check => { - check.equals(ship.getValue("hull"), 5, "hull"); - check.equals(ship.getValue("shield"), 15, "shield"); - check.equals(ship.getValue("power"), 2, "power"); - check.equals(ship.active_effects.count(), 1, "effects count"); - check.equals(ship.getAttribute("power_capacity"), 3, "power capacity"); - check.equals(ship.actions.isToggled(action2), true, "action 2 activation"); - check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); - }); + test.case("restores as new at the end of battle", check => { + let ship = new Ship(); + TestTools.setShipModel(ship, 10, 20, 5); + ship.setValue("hull", 5); + ship.setValue("shield", 15); + ship.setValue("power", 2); + ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12)); + ship.updateAttributes(); + let action1 = new BaseAction("action1"); + ship.actions.addCustom(action1); + let action2 = new ToggleAction("action2"); + ship.actions.addCustom(action2); + ship.actions.toggle(action2, true); + let action3 = new ToggleAction("action3"); + ship.actions.addCustom(action3); - ship.restoreInitialState(); - - check.in("after", check => { - check.equals(ship.getValue("hull"), 10, "hull"); - check.equals(ship.getValue("shield"), 20, "shield"); - check.equals(ship.getValue("power"), 5, "power"); - check.equals(ship.active_effects.count(), 0, "effects count"); - check.equals(ship.getAttribute("power_capacity"), 5, "power capacity"); - check.equals(ship.actions.isToggled(action2), false, "action 2 activation"); - check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); - }); - }); - - test.case("lists active effects", check => { - let ship = new Ship(); - check.equals(ship.getEffects(), []); - - let effect1 = new AttributeEffect("evasion", 4); - check.patch(ship.model, "getEffects", () => [effect1]); - check.equals(ship.getEffects(), [effect1]); - - let effect2 = new AttributeLimitEffect("evasion", 2); - ship.active_effects.add(new StickyEffect(effect2, 4)); - check.equals(ship.getEffects(), [effect1, effect2]); - }); - - test.case("gets a textual description of an attribute", check => { - let ship = new Ship(); - check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering"); - - check.patch(ship, "getUpgrades", () => [ - { code: "Base", effects: [new AttributeEffect("evasion", 3)] }, - { code: "Up1", effects: [new AttributeEffect("shield_capacity", 1)] }, - { code: "Up2", effects: [new AttributeEffect("shield_capacity", 1), new AttributeEffect("evasion", 1)] } - ]); - check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1"); - - ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 3))); - check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nSticky effect: limit to 3"); - - ship.active_effects.remove(ship.active_effects.list()[0]); - ship.active_effects.add(new AttributeEffect("evasion", -1)); - check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nActive effect: -1"); - }); - - test.case("produces death diffs", check => { - let battle = TestTools.createBattle(1); - let ship = nn(battle.playing_ship); - - check.equals(ship.getDeathDiffs(battle), [ - new ShipValueDiff(ship, "hull", -1), - new ShipDeathDiff(battle, ship), - ]); - - let effect1 = ship.active_effects.add(new AttributeEffect("shield_capacity", 2)); - let effect2 = ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 1))); - let action1 = ship.actions.addCustom(new ToggleAction("weapon1", { power: 3 })); - let action2 = ship.actions.addCustom(new ToggleAction("weapon2", { power: 3 })); - ship.actions.toggle(action2, true); - - check.equals(ship.getDeathDiffs(battle), [ - new ShipEffectRemovedDiff(ship, effect1), - new ShipAttributeDiff(ship, "shield_capacity", {}, { cumulative: 2 }), - new ShipEffectRemovedDiff(ship, effect2), - new ShipAttributeDiff(ship, "evasion", {}, { limit: 1 }), - new ShipActionToggleDiff(ship, action2, false), - new ShipValueDiff(ship, "hull", -1), - new ShipDeathDiff(battle, ship), - ]); - }); + check.in("before", check => { + check.equals(ship.getValue("hull"), 5, "hull"); + check.equals(ship.getValue("shield"), 15, "shield"); + check.equals(ship.getValue("power"), 2, "power"); + check.equals(ship.active_effects.count(), 1, "effects count"); + check.equals(ship.getAttribute("power_capacity"), 3, "power capacity"); + check.equals(ship.actions.isToggled(action2), true, "action 2 activation"); + check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); }); -} + + ship.restoreInitialState(); + + check.in("after", check => { + check.equals(ship.getValue("hull"), 10, "hull"); + check.equals(ship.getValue("shield"), 20, "shield"); + check.equals(ship.getValue("power"), 5, "power"); + check.equals(ship.active_effects.count(), 0, "effects count"); + check.equals(ship.getAttribute("power_capacity"), 5, "power capacity"); + check.equals(ship.actions.isToggled(action2), false, "action 2 activation"); + check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); + }); + }); + + test.case("lists active effects", check => { + let ship = new Ship(); + check.equals(ship.getEffects(), []); + + let effect1 = new AttributeEffect("evasion", 4); + check.patch(ship.model, "getEffects", () => [effect1]); + check.equals(ship.getEffects(), [effect1]); + + let effect2 = new AttributeLimitEffect("evasion", 2); + ship.active_effects.add(new StickyEffect(effect2, 4)); + check.equals(ship.getEffects(), [effect1, effect2]); + }); + + test.case("gets a textual description of an attribute", check => { + let ship = new Ship(); + check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering"); + + check.patch(ship, "getUpgrades", () => [ + { code: "Base", effects: [new AttributeEffect("evasion", 3)] }, + { code: "Up1", effects: [new AttributeEffect("shield_capacity", 1)] }, + { code: "Up2", effects: [new AttributeEffect("shield_capacity", 1), new AttributeEffect("evasion", 1)] } + ]); + check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1"); + + ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 3))); + check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nSticky effect: limit to 3"); + + ship.active_effects.remove(ship.active_effects.list()[0]); + ship.active_effects.add(new AttributeEffect("evasion", -1)); + check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nActive effect: -1"); + }); + + test.case("produces death diffs", check => { + let battle = TestTools.createBattle(1); + let ship = nn(battle.playing_ship); + + check.equals(ship.getDeathDiffs(battle), [ + new ShipValueDiff(ship, "hull", -1), + new ShipDeathDiff(battle, ship), + ]); + + let effect1 = ship.active_effects.add(new AttributeEffect("shield_capacity", 2)); + let effect2 = ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 1))); + let action1 = ship.actions.addCustom(new ToggleAction("weapon1", { power: 3 })); + let action2 = ship.actions.addCustom(new ToggleAction("weapon2", { power: 3 })); + ship.actions.toggle(action2, true); + + check.equals(ship.getDeathDiffs(battle), [ + new ShipEffectRemovedDiff(ship, effect1), + new ShipAttributeDiff(ship, "shield_capacity", {}, { cumulative: 2 }), + new ShipEffectRemovedDiff(ship, effect2), + new ShipAttributeDiff(ship, "evasion", {}, { limit: 1 }), + new ShipActionToggleDiff(ship, action2, false), + new ShipValueDiff(ship, "hull", -1), + new ShipDeathDiff(battle, ship), + ]); + }); +}); diff --git a/src/core/Ship.ts b/src/core/Ship.ts index f2b1687..8306bfc 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -1,441 +1,462 @@ -/// - -module TK.SpaceTac { - /** - * A single ship in a fleet - */ - export class Ship extends RObject { - // Ship model - model: ShipModel - - // Fleet this ship is a member of - fleet: Fleet - - // Level of this ship - level = new ShipLevel() - - // Name of the ship, null if unimportant - name: string | null - - // Flag indicating if the ship is alive - alive: boolean - - // Flag indicating that the ship is mission critical (escorted ship) - critical = false - - // Position in the arena - arena_x: number - arena_y: number - - // Facing direction in the arena - arena_angle: number - - // Available actions - actions = new ActionList() - - // Active effects (sticky, self or area) - active_effects = new RObjectContainer() - - // Ship attributes - attributes = new ShipAttributes() - - // Ship values - values = new ShipValues() - - // Personality - personality = new Personality() - - // Boolean set to true if the ship is currently playing its turn - playing = false - - // Priority in current battle's play_order (used as sort key) - play_priority = 0 - - // Create a new ship inside a fleet - constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel()) { - super(); - - this.fleet = fleet || new Fleet(); - this.name = name; - this.alive = true; - - this.arena_x = 0; - this.arena_y = 0; - this.arena_angle = 0; - - this.model = model; - - this.updateAttributes(); - this.actions.updateFromShip(this); - - this.fleet.addShip(this); - } - - /** - * Return the current location and angle of this ship - */ - get location(): ArenaLocationAngle { - return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle); - } - - /** - * Returns the name of this ship - */ - getName(level = true): string { - let name = this.name || this.model.name; - return level ? `Level ${this.level.get()} ${name}` : name; - } - - // Returns true if the ship is able to play - // If *check_ap* is true, ap_current=0 will make this function return false - isAbleToPlay(check_ap: boolean = true): boolean { - var ap_checked = !check_ap || this.getValue("power") > 0; - return this.alive && ap_checked; - } - - // Set position in the arena - // This does not consumes action points - setArenaPosition(x: number, y: number) { - this.arena_x = x; - this.arena_y = y; - } - - // Set facing angle in the arena - setArenaFacingAngle(angle: number) { - this.arena_angle = angle; - } - - // String repr - jasmineToString(): string { - return this.getName(); - } - - // Make an initiative throw, to resolve play order in a battle - throwInitiative(gen: RandomGenerator): void { - this.play_priority = gen.random() * this.attributes.initiative.get(); - } - - /** - * Return the player that plays this ship - */ - getPlayer(): Player { - return this.fleet.player; - } - - /** - * Check if a player is playing this ship - */ - isPlayedBy(player: Player): boolean { - return player.is(this.fleet.player); - } - - /** - * Get the battle this ship is currently engaged in - */ - getBattle(): Battle | null { - return this.fleet.battle; - } - - /** - * Get the list of activated upgrades - */ - getUpgrades(): ShipUpgrade[] { - return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades()); - } - - /** - * Refresh the actions and attributes from the bound model - */ - refreshFromModel(): void { - this.updateAttributes(); - this.actions.updateFromShip(this); - } - - /** - * Change the ship model - */ - setModel(model: ShipModel): void { - this.model = model; - this.level.clearUpgrades(); - this.refreshFromModel(); - } - - /** - * Toggle an upgrade - */ - activateUpgrade(upgrade: ShipUpgrade, on: boolean): void { - if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) { - return; - } - this.level.activateUpgrade(upgrade, on); - this.refreshFromModel(); - } - - /** - * Get the number of upgrade points available - */ - getAvailableUpgradePoints(): number { - let upgrades = this.getUpgrades(); - return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0)); - } - - /** - * Add an event to the battle log, if any - */ - addBattleEvent(event: BaseBattleDiff): void { - var battle = this.getBattle(); - if (battle && battle.log) { - battle.log.add(event); - } - } - - /** - * Get a ship value - */ - getValue(name: keyof ShipValues): number { - return this.values[name]; - } - - /** - * Set a ship value - */ - setValue(name: keyof ShipValues, value: number, relative = false): void { - if (relative) { - value += this.values[name]; - } - this.values[name] = value; - } - - /** - * Get a ship attribute's current value - */ - getAttribute(name: keyof ShipAttributes): number { - if (!this.attributes.hasOwnProperty(name)) { - console.error(`No such ship attribute: ${name}`); - return 0; - } - return this.attributes[name].get(); - } - - /** - * Initialize the action points counter - * This should be called once at the start of a battle - * If no value is provided, the attribute power_capacity will be used - */ - private initializePower(value: number | null = null): void { - if (value === null) { - value = this.getAttribute("power_capacity"); - } - this.setValue("power", value); - } - - /** - * Method called at the start of battle, to restore a pristine condition on the ship - */ - restoreInitialState() { - this.alive = true; - - this.actions.updateFromShip(this); - this.active_effects = new RObjectContainer(); - - this.updateAttributes(); - this.restoreHealth(); - this.initializePower(); - } - - /** - * Check if the ship is inside a given circular area - */ - isInCircle(x: number, y: number, radius: number): boolean { - let dx = this.arena_x - x; - let dy = this.arena_y - y; - let distance = Math.sqrt(dx * dx + dy * dy); - return distance <= radius; - } - - /** - * Get the distance to another ship - */ - getDistanceTo(other: Ship): number { - return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other)); - } - - /** - * Get the diffs needed to apply changes to a ship value - */ - getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - let current = this.values[name]; - - if (relative) { - value += current; - } - - // TODO apply range limitations - - if (current != value) { - result.push(new ShipValueDiff(this, name, value - current)); - } - - return result; - } - - /** - * Produce diffs needed to put the ship in emergency stasis - */ - getDeathDiffs(battle: Battle): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - // Remove active effects - this.active_effects.list().forEach(effect => { - if (!(effect instanceof StickyEffect)) { - result.push(new ShipEffectRemovedDiff(this, effect)); - } - result = result.concat(effect.getOffDiffs(this)); - }); - - // Deactivate toggle actions - this.getToggleActions(true).forEach(action => { - result = result.concat(action.getSpecificDiffs(this, battle, Target.newFromShip(this))); - }); - - // Put all values to 0 - keys(SHIP_VALUES).forEach(value => { - result = result.concat(this.getValueDiffs(value, 0)); - }); - - // Mark as dead - result.push(new ShipDeathDiff(battle, this)); - - return result; - } - - /** - * Set the death status on this ship - */ - setDead(): void { - let battle = this.getBattle(); - if (battle) { - let events = this.getDeathDiffs(battle); - battle.applyDiffs(events); - } else { - console.error("Cannot set ship dead outside of battle", this); - } - } - - /** - * Update attributes, taking into account model's permanent effects and active effects - */ - updateAttributes(): void { - // Reset attributes - keys(this.attributes).forEach(attr => this.attributes[attr].reset()); - - // Apply attribute effects - this.getEffects().forEach(effect => { - if (effect instanceof AttributeEffect) { - this.attributes[effect.attrcode].addModifier(effect.value); - } else if (effect instanceof AttributeMultiplyEffect) { - this.attributes[effect.attrcode].addModifier(undefined, effect.value); - } else if (effect instanceof AttributeLimitEffect) { - this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value); - } - }); - } - - /** - * Fully restore hull and shield, at their maximal capacity - */ - restoreHealth(): void { - if (this.alive) { - this.setValue("hull", this.getAttribute("hull_capacity")); - this.setValue("shield", this.getAttribute("shield_capacity")); - } - } - - /** - * Get actions from the ship model - */ - getModelActions(): BaseAction[] { - return this.model.getActions(this.level.get(), this.level.getUpgrades()); - } - - /** - * Get permanent effects from the ship model - */ - getModelEffects(): BaseEffect[] { - return this.model.getEffects(this.level.get(), this.level.getUpgrades()); - } - - /** - * Iterator over all effects active for this ship. - * - * This combines the permanent effects from ship model, with sticky and area effects. - */ - getEffects(): BaseEffect[] { - return this.getModelEffects().concat( - this.active_effects.list().map(effect => (effect instanceof StickyEffect) ? effect.base : effect) - ); - } - - /** - * Iterator over toggle actions - */ - getToggleActions(only_active = false): ToggleAction[] { - let result = cfilter(this.actions.listAll(), ToggleAction); - if (only_active) { - result = result.filter(action => this.actions.isToggled(action)); - } - return result; - } - - /** - * Get the effects that this ship has on another ship (which may be herself) - */ - getAreaEffects(ship: Ship): BaseEffect[] { - let toggled = this.getToggleActions(true); - let effects = toggled.map(action => { - if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) { - return action.effects; - } else { - return []; - } - }); - return flatten(effects); - } - - /** - * Get a textual description of an attribute, and the origin of its value - */ - getAttributeDescription(attribute: keyof ShipAttributes): string { - let result = SHIP_VALUES_DESCRIPTIONS[attribute]; - - let diffs: string[] = []; - let limits: string[] = []; - - function addEffect(base: string, effect: BaseEffect) { - if (effect instanceof AttributeEffect && effect.attrcode == attribute) { - diffs.push(`${base}: ${effect.value > 0 ? "+" + effect.value.toString() : effect.value}`); - } else if (effect instanceof AttributeLimitEffect && effect.attrcode == attribute) { - limits.push(`${base}: limit to ${effect.value}`); - } - } - - this.getUpgrades().forEach(upgrade => { - if (upgrade.effects) { - upgrade.effects.forEach(effect => addEffect(upgrade.code, effect)); - } - }); - - this.active_effects.list().forEach(effect => { - if (effect instanceof StickyEffect) { - addEffect("Sticky effect", effect.base); - } else { - addEffect("Active effect", effect); - } - }); - - let sources = diffs.concat(limits).join("\n"); - return sources ? (result + "\n\n" + sources) : result; - } +import { RandomGenerator } from "../common/RandomGenerator" +import { RObject, RObjectContainer } from "../common/RObject" +import { bool, cfilter, flatten, keys, sum } from "../common/Tools" +import { ActionList } from "./actions/ActionList" +import { BaseAction } from "./actions/BaseAction" +import { ToggleAction } from "./actions/ToggleAction" +import { ArenaLocationAngle } from "./ArenaLocation" +import { Battle } from "./Battle" +import { BaseBattleDiff } from "./diffs/BaseBattleDiff" +import { ShipDeathDiff } from "./diffs/ShipDeathDiff" +import { ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff" +import { ShipValueDiff } from "./diffs/ShipValueDiff" +import { AttributeEffect } from "./effects/AttributeEffect" +import { AttributeLimitEffect } from "./effects/AttributeLimitEffect" +import { AttributeMultiplyEffect } from "./effects/AttributeMultiplyEffect" +import { BaseEffect } from "./effects/BaseEffect" +import { StickyEffect } from "./effects/StickyEffect" +import { Fleet } from "./Fleet" +import { ShipModel, ShipUpgrade } from "./models/ShipModel" +import { Personality } from "./Personality" +import { Player } from "./Player" +import { ShipLevel } from "./ShipLevel" +import { ShipAttributes, ShipValues, SHIP_VALUES, SHIP_VALUES_DESCRIPTIONS } from "./ShipValue" +import { Target } from "./Target" + +/** + * A single ship in a fleet + */ +export class Ship extends RObject { + // Ship model + model: ShipModel + + // Fleet this ship is a member of + fleet: Fleet + + // Level of this ship + level = new ShipLevel() + + // Name of the ship, null if unimportant + name: string | null + + // Flag indicating if the ship is alive + alive: boolean + + // Flag indicating that the ship is mission critical (escorted ship) + critical = false + + // Position in the arena + arena_x: number + arena_y: number + + // Facing direction in the arena + arena_angle: number + + // Available actions + actions = new ActionList() + + // Active effects (sticky, self or area) + active_effects = new RObjectContainer() + + // Ship attributes + attributes = new ShipAttributes() + + // Ship values + values = new ShipValues() + + // Personality + personality = new Personality() + + // Boolean set to true if the ship is currently playing its turn + playing = false + + // Priority in current battle's play_order (used as sort key) + play_priority = 0 + + // Create a new ship inside a fleet + constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel()) { + super(); + + this.fleet = fleet || new Fleet(); + this.name = name; + this.alive = true; + + this.arena_x = 0; + this.arena_y = 0; + this.arena_angle = 0; + + this.model = model; + + this.updateAttributes(); + this.actions.updateFromShip(this); + + this.fleet.addShip(this); + } + + /** + * Return the current location and angle of this ship + */ + get location(): ArenaLocationAngle { + return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle); + } + + /** + * Returns the name of this ship + */ + getName(level = true): string { + let name = this.name || this.model.name; + return level ? `Level ${this.level.get()} ${name}` : name; + } + + // Returns true if the ship is able to play + // If *check_ap* is true, ap_current=0 will make this function return false + isAbleToPlay(check_ap: boolean = true): boolean { + var ap_checked = !check_ap || this.getValue("power") > 0; + return this.alive && ap_checked; + } + + // Set position in the arena + // This does not consumes action points + setArenaPosition(x: number, y: number) { + this.arena_x = x; + this.arena_y = y; + } + + // Set facing angle in the arena + setArenaFacingAngle(angle: number) { + this.arena_angle = angle; + } + + // String repr + jasmineToString(): string { + return this.getName(); + } + + // Make an initiative throw, to resolve play order in a battle + throwInitiative(gen: RandomGenerator): void { + this.play_priority = gen.random() * this.attributes.initiative.get(); + } + + /** + * Return the player that plays this ship + */ + getPlayer(): Player { + return this.fleet.player; + } + + /** + * Check if a player is playing this ship + */ + isPlayedBy(player: Player): boolean { + return player.is(this.fleet.player); + } + + /** + * Get the battle this ship is currently engaged in + */ + getBattle(): Battle | null { + return this.fleet.battle; + } + + /** + * Get the list of activated upgrades + */ + getUpgrades(): ShipUpgrade[] { + return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades()); + } + + /** + * Refresh the actions and attributes from the bound model + */ + refreshFromModel(): void { + this.updateAttributes(); + this.actions.updateFromShip(this); + } + + /** + * Change the ship model + */ + setModel(model: ShipModel): void { + this.model = model; + this.level.clearUpgrades(); + this.refreshFromModel(); + } + + /** + * Toggle an upgrade + */ + activateUpgrade(upgrade: ShipUpgrade, on: boolean): void { + if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) { + return; } + this.level.activateUpgrade(upgrade, on); + this.refreshFromModel(); + } + + /** + * Get the number of upgrade points available + */ + getAvailableUpgradePoints(): number { + let upgrades = this.getUpgrades(); + return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0)); + } + + /** + * Add an event to the battle log, if any + */ + addBattleEvent(event: BaseBattleDiff): void { + var battle = this.getBattle(); + if (battle && battle.log) { + battle.log.add(event); + } + } + + /** + * Get a ship value + */ + getValue(name: keyof ShipValues): number { + return this.values[name]; + } + + /** + * Set a ship value + */ + setValue(name: keyof ShipValues, value: number, relative = false): void { + if (relative) { + value += this.values[name]; + } + this.values[name] = value; + } + + /** + * Get a ship attribute's current value + */ + getAttribute(name: keyof ShipAttributes): number { + if (!this.attributes.hasOwnProperty(name)) { + console.error(`No such ship attribute: ${name}`); + return 0; + } + return this.attributes[name].get(); + } + + /** + * Initialize the action points counter + * This should be called once at the start of a battle + * If no value is provided, the attribute power_capacity will be used + */ + private initializePower(value: number | null = null): void { + if (value === null) { + value = this.getAttribute("power_capacity"); + } + this.setValue("power", value); + } + + /** + * Method called at the start of battle, to restore a pristine condition on the ship + */ + restoreInitialState() { + this.alive = true; + + this.actions.updateFromShip(this); + this.active_effects = new RObjectContainer(); + + this.updateAttributes(); + this.restoreHealth(); + this.initializePower(); + } + + /** + * Check if the ship is inside a given circular area + */ + isInCircle(x: number, y: number, radius: number): boolean { + let dx = this.arena_x - x; + let dy = this.arena_y - y; + let distance = Math.sqrt(dx * dx + dy * dy); + return distance <= radius; + } + + /** + * Get the distance to another ship + */ + getDistanceTo(other: Ship): number { + return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other)); + } + + /** + * Get the diffs needed to apply changes to a ship value + */ + getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + let current = this.values[name]; + + if (relative) { + value += current; + } + + // TODO apply range limitations + + if (current != value) { + result.push(new ShipValueDiff(this, name, value - current)); + } + + return result; + } + + /** + * Produce diffs needed to put the ship in emergency stasis + */ + getDeathDiffs(battle: Battle): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + // Remove active effects + this.active_effects.list().forEach(effect => { + if (!(effect instanceof StickyEffect)) { + result.push(new ShipEffectRemovedDiff(this, effect)); + } + result = result.concat(effect.getOffDiffs(this)); + }); + + // Deactivate toggle actions + this.getToggleActions(true).forEach(action => { + result = result.concat(action.getSpecificDiffs(this, battle, Target.newFromShip(this))); + }); + + // Put all values to 0 + keys(SHIP_VALUES).forEach(value => { + result = result.concat(this.getValueDiffs(value, 0)); + }); + + // Mark as dead + result.push(new ShipDeathDiff(battle, this)); + + return result; + } + + /** + * Set the death status on this ship + */ + setDead(): void { + let battle = this.getBattle(); + if (battle) { + let events = this.getDeathDiffs(battle); + battle.applyDiffs(events); + } else { + console.error("Cannot set ship dead outside of battle", this); + } + } + + /** + * Update attributes, taking into account model's permanent effects and active effects + */ + updateAttributes(): void { + // Reset attributes + keys(this.attributes).forEach(attr => this.attributes[attr].reset()); + + // Apply attribute effects + this.getEffects().forEach(effect => { + if (effect instanceof AttributeEffect) { + this.attributes[effect.attrcode].addModifier(effect.value); + } else if (effect instanceof AttributeMultiplyEffect) { + this.attributes[effect.attrcode].addModifier(undefined, effect.value); + } else if (effect instanceof AttributeLimitEffect) { + this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value); + } + }); + } + + /** + * Fully restore hull and shield, at their maximal capacity + */ + restoreHealth(): void { + if (this.alive) { + this.setValue("hull", this.getAttribute("hull_capacity")); + this.setValue("shield", this.getAttribute("shield_capacity")); + } + } + + /** + * Get actions from the ship model + */ + getModelActions(): BaseAction[] { + return this.model.getActions(this.level.get(), this.level.getUpgrades()); + } + + /** + * Get permanent effects from the ship model + */ + getModelEffects(): BaseEffect[] { + return this.model.getEffects(this.level.get(), this.level.getUpgrades()); + } + + /** + * Iterator over all effects active for this ship. + * + * This combines the permanent effects from ship model, with sticky and area effects. + */ + getEffects(): BaseEffect[] { + return this.getModelEffects().concat( + this.active_effects.list().map(effect => (effect instanceof StickyEffect) ? effect.base : effect) + ); + } + + /** + * Iterator over toggle actions + */ + getToggleActions(only_active = false): ToggleAction[] { + let result = cfilter(this.actions.listAll(), ToggleAction); + if (only_active) { + result = result.filter(action => this.actions.isToggled(action)); + } + return result; + } + + /** + * Get the effects that this ship has on another ship (which may be herself) + */ + getAreaEffects(ship: Ship): BaseEffect[] { + let toggled = this.getToggleActions(true); + let effects = toggled.map(action => { + if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) { + return action.effects; + } else { + return []; + } + }); + return flatten(effects); + } + + /** + * Get a textual description of an attribute, and the origin of its value + */ + getAttributeDescription(attribute: keyof ShipAttributes): string { + let result = SHIP_VALUES_DESCRIPTIONS[attribute]; + + let diffs: string[] = []; + let limits: string[] = []; + + function addEffect(base: string, effect: BaseEffect) { + if (effect instanceof AttributeEffect && effect.attrcode == attribute) { + diffs.push(`${base}: ${effect.value > 0 ? "+" + effect.value.toString() : effect.value}`); + } else if (effect instanceof AttributeLimitEffect && effect.attrcode == attribute) { + limits.push(`${base}: limit to ${effect.value}`); + } + } + + this.getUpgrades().forEach(upgrade => { + if (upgrade.effects) { + upgrade.effects.forEach(effect => addEffect(upgrade.code, effect)); + } + }); + + this.active_effects.list().forEach(effect => { + if (effect instanceof StickyEffect) { + addEffect("Sticky effect", effect.base); + } else { + addEffect("Active effect", effect); + } + }); + + let sources = diffs.concat(limits).join("\n"); + return sources ? (result + "\n\n" + sources) : result; + } } diff --git a/src/core/ShipGenerator.spec.ts b/src/core/ShipGenerator.spec.ts index cafee09..f97f17f 100644 --- a/src/core/ShipGenerator.spec.ts +++ b/src/core/ShipGenerator.spec.ts @@ -1,11 +1,13 @@ -module TK.SpaceTac.Specs { - testing("ShipGenerator", test => { - test.case("can use ship model", check => { - var gen = new ShipGenerator(); - var model = new ShipModel("test", "Test"); - var ship = gen.generate(3, model, false); - check.same(ship.model, model); - check.same(ship.level.get(), 3); - }); - }); -} +import { testing } from "../common/Testing"; +import { ShipModel } from "./models/ShipModel"; +import { ShipGenerator } from "./ShipGenerator"; + +testing("ShipGenerator", test => { + test.case("can use ship model", check => { + var gen = new ShipGenerator(); + var model = new ShipModel("test", "Test"); + var ship = gen.generate(3, model, false); + check.same(ship.model, model); + check.same(ship.level.get(), 3); + }); +}); diff --git a/src/core/ShipGenerator.ts b/src/core/ShipGenerator.ts index ebfcbf5..884f754 100644 --- a/src/core/ShipGenerator.ts +++ b/src/core/ShipGenerator.ts @@ -1,49 +1,51 @@ -module TK.SpaceTac { - /** - * Generator of random ship - */ - export class ShipGenerator { - // Random number generator used - random: RandomGenerator +import { RandomGenerator } from "../common/RandomGenerator"; +import { ShipModel } from "./models/ShipModel"; +import { Ship } from "./Ship"; - constructor(random = RandomGenerator.global) { - this.random = random; - } +/** + * Generator of random ship + */ +export class ShipGenerator { + // Random number generator used + random: RandomGenerator - /** - * Generate a ship of a givel level. - * - * If *upgrade* is true, random levelling options will be chosen - */ - generate(level: number, model: ShipModel | null = null, upgrade = true): Ship { - if (!model) { - // Get a random model - model = ShipModel.getRandomModel(level, this.random); - } + constructor(random = RandomGenerator.global) { + this.random = random; + } - let result = new Ship(null, null, model); - - result.level.forceLevel(level); - if (upgrade) { - let iteration = 0; - while (iteration < 100) { - iteration += 1; - - let points = result.getAvailableUpgradePoints(); - let upgrades = model.getAvailableUpgrades(result.level.get()).filter(upgrade => { - return (upgrade.cost || 0) <= points && !result.level.hasUpgrade(upgrade); - }); - - if (upgrades.length > 0) { - let upgrade = this.random.choice(upgrades); - result.activateUpgrade(upgrade, true); - } else { - break; - } - } - } - - return result; - } + /** + * Generate a ship of a givel level. + * + * If *upgrade* is true, random levelling options will be chosen + */ + generate(level: number, model: ShipModel | null = null, upgrade = true): Ship { + if (!model) { + // Get a random model + model = ShipModel.getRandomModel(level, this.random); } + + let result = new Ship(null, null, model); + + result.level.forceLevel(level); + if (upgrade) { + let iteration = 0; + while (iteration < 100) { + iteration += 1; + + let points = result.getAvailableUpgradePoints(); + let upgrades = model.getAvailableUpgrades(result.level.get()).filter(upgrade => { + return (upgrade.cost || 0) <= points && !result.level.hasUpgrade(upgrade); + }); + + if (upgrades.length > 0) { + let upgrade = this.random.choice(upgrades); + result.activateUpgrade(upgrade, true); + } else { + break; + } + } + } + + return result; + } } diff --git a/src/core/ShipLevel.spec.ts b/src/core/ShipLevel.spec.ts index 0efae42..ed6b1fd 100644 --- a/src/core/ShipLevel.spec.ts +++ b/src/core/ShipLevel.spec.ts @@ -1,70 +1,71 @@ -module TK.SpaceTac.Specs { - testing("ShipLevel", test => { - test.case("level up from experience points", check => { - let level = new ShipLevel(); - check.equals(level.get(), 1); - check.equals(level.getNextGoal(), 100); - check.equals(level.getUpgradePoints(), 0); +import { testing } from "../common/Testing"; +import { ShipLevel } from "./ShipLevel"; - level.addExperience(60); // 60 - check.equals(level.get(), 1); - check.equals(level.checkLevelUp(), false); +testing("ShipLevel", test => { + test.case("level up from experience points", check => { + let level = new ShipLevel(); + check.equals(level.get(), 1); + check.equals(level.getNextGoal(), 100); + check.equals(level.getUpgradePoints(), 0); - level.addExperience(70); // 130 - check.equals(level.get(), 1); - check.equals(level.checkLevelUp(), true); - check.equals(level.get(), 2); - check.equals(level.getNextGoal(), 300); - check.equals(level.getUpgradePoints(), 3); + level.addExperience(60); // 60 + check.equals(level.get(), 1); + check.equals(level.checkLevelUp(), false); - level.addExperience(200); // 330 - check.equals(level.get(), 2); - check.equals(level.checkLevelUp(), true); - check.equals(level.get(), 3); - check.equals(level.getNextGoal(), 600); - check.equals(level.getUpgradePoints(), 5); + level.addExperience(70); // 130 + check.equals(level.get(), 1); + check.equals(level.checkLevelUp(), true); + check.equals(level.get(), 2); + check.equals(level.getNextGoal(), 300); + check.equals(level.getUpgradePoints(), 3); - level.addExperience(320); // 650 - check.equals(level.get(), 3); - check.equals(level.checkLevelUp(), true); - check.equals(level.get(), 4); - check.equals(level.getNextGoal(), 1000); - check.equals(level.getUpgradePoints(), 7); - }); + level.addExperience(200); // 330 + check.equals(level.get(), 2); + check.equals(level.checkLevelUp(), true); + check.equals(level.get(), 3); + check.equals(level.getNextGoal(), 600); + check.equals(level.getUpgradePoints(), 5); - test.case("forces a given level", check => { - let level = new ShipLevel(); - check.equals(level.get(), 1); + level.addExperience(320); // 650 + check.equals(level.get(), 3); + check.equals(level.checkLevelUp(), true); + check.equals(level.get(), 4); + check.equals(level.getNextGoal(), 1000); + check.equals(level.getUpgradePoints(), 7); + }); - level.forceLevel(10); - check.equals(level.get(), 10); - }); + test.case("forces a given level", check => { + let level = new ShipLevel(); + check.equals(level.get(), 1); - test.case("manages upgrades", check => { - let up1 = { code: "test1" }; - let up2 = { code: "test2" }; + level.forceLevel(10); + check.equals(level.get(), 10); + }); - let level = new ShipLevel(); - check.equals(level.getUpgrades(), []); - check.equals(level.hasUpgrade(up1), false); + test.case("manages upgrades", check => { + let up1 = { code: "test1" }; + let up2 = { code: "test2" }; - level.activateUpgrade(up1, true); - check.equals(level.getUpgrades(), ["test1"]); - check.equals(level.hasUpgrade(up1), true); + let level = new ShipLevel(); + check.equals(level.getUpgrades(), []); + check.equals(level.hasUpgrade(up1), false); - level.activateUpgrade(up1, true); - check.equals(level.getUpgrades(), ["test1"]); - check.equals(level.hasUpgrade(up1), true); + level.activateUpgrade(up1, true); + check.equals(level.getUpgrades(), ["test1"]); + check.equals(level.hasUpgrade(up1), true); - level.activateUpgrade(up1, false); - check.equals(level.getUpgrades(), []); - check.equals(level.hasUpgrade(up1), false); + level.activateUpgrade(up1, true); + check.equals(level.getUpgrades(), ["test1"]); + check.equals(level.hasUpgrade(up1), true); - level.activateUpgrade(up1, true); - level.activateUpgrade(up2, true); - check.equals(level.getUpgrades(), ["test1", "test2"]); - level.clearUpgrades(); - check.equals(level.getUpgrades(), []); - }); - }); -} + level.activateUpgrade(up1, false); + check.equals(level.getUpgrades(), []); + check.equals(level.hasUpgrade(up1), false); + + level.activateUpgrade(up1, true); + level.activateUpgrade(up2, true); + check.equals(level.getUpgrades(), ["test1", "test2"]); + level.clearUpgrades(); + check.equals(level.getUpgrades(), []); + }); +}); diff --git a/src/core/ShipLevel.ts b/src/core/ShipLevel.ts index 5c8c226..076eead 100644 --- a/src/core/ShipLevel.ts +++ b/src/core/ShipLevel.ts @@ -1,119 +1,121 @@ -module TK.SpaceTac { - /** - * Level and experience system for a ship, with enabled upgrades. - */ - export class ShipLevel { - private level = 1 - private experience = 0 - private upgrades: string[] = [] +import { imap, irange, isum } from "../common/Iterators"; +import { acopy, add, contains, remove } from "../common/Tools"; +import { ShipUpgrade } from "./models/ShipModel"; - /** - * Get current level - */ - get(): number { - return this.level; - } +/** + * Level and experience system for a ship, with enabled upgrades. + */ +export class ShipLevel { + private level = 1 + private experience = 0 + private upgrades: string[] = [] - /** - * Get the current experience points - */ - getExperience(): number { - return this.experience; - } + /** + * Get current level + */ + get(): number { + return this.level; + } - /** - * Get the activated upgrades - */ - getUpgrades(): string[] { - return acopy(this.upgrades); - } + /** + * Get the current experience points + */ + getExperience(): number { + return this.experience; + } - /** - * Get the next experience goal to reach, to gain one level - */ - getNextGoal(): number { - return isum(imap(irange(this.level), i => (i + 1) * 100)); - } + /** + * Get the activated upgrades + */ + getUpgrades(): string[] { + return acopy(this.upgrades); + } - /** - * Force experience gain, to reach a given level - */ - forceLevel(level: number): void { - while (this.level < level) { - this.forceLevelUp(); - } - } + /** + * Get the next experience goal to reach, to gain one level + */ + getNextGoal(): number { + return isum(imap(irange(this.level), i => (i + 1) * 100)); + } - /** - * Force a level up - */ - forceLevelUp(): void { - let old_level = this.level; - - this.addExperience(this.getNextGoal() - this.experience); - this.checkLevelUp(); - - if (old_level >= this.level) { - // security against infinite loops - throw new Error("No effective level up"); - } - } - - /** - * Check for level-up - * - * Returns true if level changed - */ - checkLevelUp(): boolean { - let changed = false; - while (this.experience >= this.getNextGoal()) { - this.level++; - changed = true; - } - return changed; - } - - /** - * Add experience points - */ - addExperience(points: number): void { - this.experience += points; - } - - /** - * Get upgrade points given by current level - * - * This does not deduce activated upgrades usage - */ - getUpgradePoints(): number { - return this.level > 1 ? (1 + 2 * (this.level - 1)) : 0; - } - - /** - * (De)Activate an upgrade - * - * This does not check the upgrade points needed - */ - activateUpgrade(upgrade: ShipUpgrade, active: boolean): void { - if (active) { - add(this.upgrades, upgrade.code); - } else { - remove(this.upgrades, upgrade.code); - } - } - - /** - * Check if an upgrade is active - */ - hasUpgrade(upgrade: ShipUpgrade): boolean { - return contains(this.upgrades, upgrade.code); - } - - /** - * Clear all activated upgrades - */ - clearUpgrades(): void { - this.upgrades = []; - } + /** + * Force experience gain, to reach a given level + */ + forceLevel(level: number): void { + while (this.level < level) { + this.forceLevelUp(); } + } + + /** + * Force a level up + */ + forceLevelUp(): void { + let old_level = this.level; + + this.addExperience(this.getNextGoal() - this.experience); + this.checkLevelUp(); + + if (old_level >= this.level) { + // security against infinite loops + throw new Error("No effective level up"); + } + } + + /** + * Check for level-up + * + * Returns true if level changed + */ + checkLevelUp(): boolean { + let changed = false; + while (this.experience >= this.getNextGoal()) { + this.level++; + changed = true; + } + return changed; + } + + /** + * Add experience points + */ + addExperience(points: number): void { + this.experience += points; + } + + /** + * Get upgrade points given by current level + * + * This does not deduce activated upgrades usage + */ + getUpgradePoints(): number { + return this.level > 1 ? (1 + 2 * (this.level - 1)) : 0; + } + + /** + * (De)Activate an upgrade + * + * This does not check the upgrade points needed + */ + activateUpgrade(upgrade: ShipUpgrade, active: boolean): void { + if (active) { + add(this.upgrades, upgrade.code); + } else { + remove(this.upgrades, upgrade.code); + } + } + + /** + * Check if an upgrade is active + */ + hasUpgrade(upgrade: ShipUpgrade): boolean { + return contains(this.upgrades, upgrade.code); + } + + /** + * Clear all activated upgrades + */ + clearUpgrades(): void { + this.upgrades = []; + } } diff --git a/src/core/ShipValue.spec.ts b/src/core/ShipValue.spec.ts index 950b471..e09dcaf 100644 --- a/src/core/ShipValue.spec.ts +++ b/src/core/ShipValue.spec.ts @@ -1,47 +1,48 @@ -module TK.SpaceTac { - testing("ShipAttribute", test => { - test.case("applies cumulative, multiplier and limit", check => { - let attribute = new ShipAttribute(); - check.equals(attribute.get(), 0, "initial"); +import { testing } from "../common/Testing"; +import { ShipAttribute } from "./ShipValue"; - attribute.addModifier(4); - check.in("+4", check => { - check.equals(attribute.get(), 4, "effective value"); - }); +testing("ShipAttribute", test => { + test.case("applies cumulative, multiplier and limit", check => { + let attribute = new ShipAttribute(); + check.equals(attribute.get(), 0, "initial"); - attribute.addModifier(2); - check.in("+4 +2", check => { - check.equals(attribute.get(), 6, "effective value"); - }); - - attribute.addModifier(undefined, 20); - check.in("+4 +2 +20%", check => { - check.equals(attribute.get(), 7, "effective value"); - }); - - attribute.addModifier(undefined, 5); - check.in("+4 +2 +20% +5%", check => { - check.equals(attribute.get(), 8, "effective value"); - check.equals(attribute.getMaximal(), Infinity, "maximal value"); - }); - - attribute.addModifier(undefined, undefined, 6); - check.in("+4 +2 +20% +5% lim6", check => { - check.equals(attribute.get(), 6, "effective value"); - check.equals(attribute.getMaximal(), 6, "maximal value"); - }); - - attribute.addModifier(undefined, undefined, 4); - check.in("+4 +2 +20% +5% lim6 lim4", check => { - check.equals(attribute.get(), 4, "effective value"); - check.equals(attribute.getMaximal(), 4, "maximal value"); - }); - - attribute.addModifier(undefined, undefined, 10); - check.in("+4 +2 +20% +5% lim6 lim4 lim10", check => { - check.equals(attribute.get(), 4, "effective value"); - check.equals(attribute.getMaximal(), 4, "maximal value"); - }); - }); + attribute.addModifier(4); + check.in("+4", check => { + check.equals(attribute.get(), 4, "effective value"); }); -} + + attribute.addModifier(2); + check.in("+4 +2", check => { + check.equals(attribute.get(), 6, "effective value"); + }); + + attribute.addModifier(undefined, 20); + check.in("+4 +2 +20%", check => { + check.equals(attribute.get(), 7, "effective value"); + }); + + attribute.addModifier(undefined, 5); + check.in("+4 +2 +20% +5%", check => { + check.equals(attribute.get(), 8, "effective value"); + check.equals(attribute.getMaximal(), Infinity, "maximal value"); + }); + + attribute.addModifier(undefined, undefined, 6); + check.in("+4 +2 +20% +5% lim6", check => { + check.equals(attribute.get(), 6, "effective value"); + check.equals(attribute.getMaximal(), 6, "maximal value"); + }); + + attribute.addModifier(undefined, undefined, 4); + check.in("+4 +2 +20% +5% lim6 lim4", check => { + check.equals(attribute.get(), 4, "effective value"); + check.equals(attribute.getMaximal(), 4, "maximal value"); + }); + + attribute.addModifier(undefined, undefined, 10); + check.in("+4 +2 +20% +5% lim6 lim4 lim10", check => { + check.equals(attribute.get(), 4, "effective value"); + check.equals(attribute.getMaximal(), 4, "maximal value"); + }); + }); +}); diff --git a/src/core/ShipValue.ts b/src/core/ShipValue.ts index e6d8fdd..37a7656 100644 --- a/src/core/ShipValue.ts +++ b/src/core/ShipValue.ts @@ -1,155 +1,155 @@ -module TK.SpaceTac { - type ShipValuesMapping = { - [P in (keyof ShipValues | keyof ShipAttributes)]: string - } +import { min, remove, sum } from "../common/Tools" - export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = { - "initiative": "Capacity to play before others in a battle", - "hull": "Physical structure of the ship", - "shield": "Shield around the ship that may absorb damage", - "power": "Power available to supply the equipments", - "hull_capacity": "Maximal Hull value before the ship risks collapsing", - "shield_capacity": "Maximal Shield value to protect the hull from damage", - "power_capacity": "Maximal Power value to use equipment", - "evasion": "Damage points that may be evaded by maneuvering", - } - - export const SHIP_VALUES_NAMES: ShipValuesMapping = { - "initiative": "initiative", - "hull": "hull", - "shield": "shield", - "power": "power", - "hull_capacity": "hull capacity", - "shield_capacity": "shield capacity", - "power_capacity": "power capacity", - "evasion": "evasion", - } - - /** - * A ship attribute is a number resulting of a list of modifiers. - */ - export class ShipAttribute { - // Current value - private current = 0 - - // Modifiers - private cumulatives: number[] = [] - private multipliers: number[] = [] - private limits: number[] = [] - - /** - * Get the current value - */ - get(): number { - return this.current; - } - - /** - * Get the maximal value enforced by limit modifiers, Infinity for unlimited - */ - getMaximal(): number { - if (this.limits.length > 0) { - return min(this.limits); - } else { - return Infinity; - } - } - - /** - * Reset all modifiers - */ - reset(): void { - this.cumulatives = []; - this.multipliers = []; - this.limits = []; - this.update(); - } - - /** - * Add a modifier - */ - addModifier(cumulative?: number, multiplier?: number, limit?: number): void { - if (typeof cumulative != "undefined") { - this.cumulatives.push(cumulative); - } - if (typeof multiplier != "undefined") { - this.multipliers.push(multiplier); - } - if (typeof limit != "undefined") { - this.limits.push(limit); - } - this.update(); - } - - /** - * Remove a modifier - */ - removeModifier(cumulative?: number, multiplier?: number, limit?: number): void { - if (typeof cumulative != "undefined") { - remove(this.cumulatives, cumulative); - } - if (typeof multiplier != "undefined") { - remove(this.multipliers, multiplier); - } - if (typeof limit != "undefined") { - remove(this.limits, limit); - } - this.update(); - } - - /** - * Update the current value - */ - private update(): void { - let value = sum(this.cumulatives); - if (this.multipliers.length) { - value = Math.round(value * (1 + sum(this.multipliers) / 100)); - } - if (this.limits.length) { - value = Math.min(value, min(this.limits)); - } - this.current = value; - } - } - - /** - * Set of ShipAttribute for a ship - */ - export class ShipAttributes { - // Initiative (capacity to play first) - initiative = new ShipAttribute() - // Maximal hull value - hull_capacity = new ShipAttribute() - // Maximal shield value - shield_capacity = new ShipAttribute() - // Damage evasion - evasion = new ShipAttribute() - // Maximal power value - power_capacity = new ShipAttribute() - } - - /** - * Set of simple values for a ship - */ - export class ShipValues { - hull = 0 - shield = 0 - power = 0 - } - - /** - * Static attributes and values object for property queries - */ - export const SHIP_ATTRIBUTES = new ShipAttributes(); - export const SHIP_VALUES = new ShipValues(); - - /** - * Type guards - */ - export function isShipValue(key: string): key is keyof ShipValues { - return SHIP_VALUES.hasOwnProperty(key); - } - export function isShipAttribute(key: string): key is keyof ShipAttributes { - return SHIP_ATTRIBUTES.hasOwnProperty(key); - } +type ShipValuesMapping = { + [P in (keyof ShipValues | keyof ShipAttributes)]: string +} + +export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = { + "initiative": "Capacity to play before others in a battle", + "hull": "Physical structure of the ship", + "shield": "Shield around the ship that may absorb damage", + "power": "Power available to supply the equipments", + "hull_capacity": "Maximal Hull value before the ship risks collapsing", + "shield_capacity": "Maximal Shield value to protect the hull from damage", + "power_capacity": "Maximal Power value to use equipment", + "evasion": "Damage points that may be evaded by maneuvering", +} + +export const SHIP_VALUES_NAMES: ShipValuesMapping = { + "initiative": "initiative", + "hull": "hull", + "shield": "shield", + "power": "power", + "hull_capacity": "hull capacity", + "shield_capacity": "shield capacity", + "power_capacity": "power capacity", + "evasion": "evasion", +} + +/** + * A ship attribute is a number resulting of a list of modifiers. + */ +export class ShipAttribute { + // Current value + private current = 0 + + // Modifiers + private cumulatives: number[] = [] + private multipliers: number[] = [] + private limits: number[] = [] + + /** + * Get the current value + */ + get(): number { + return this.current; + } + + /** + * Get the maximal value enforced by limit modifiers, Infinity for unlimited + */ + getMaximal(): number { + if (this.limits.length > 0) { + return min(this.limits); + } else { + return Infinity; + } + } + + /** + * Reset all modifiers + */ + reset(): void { + this.cumulatives = []; + this.multipliers = []; + this.limits = []; + this.update(); + } + + /** + * Add a modifier + */ + addModifier(cumulative?: number, multiplier?: number, limit?: number): void { + if (typeof cumulative != "undefined") { + this.cumulatives.push(cumulative); + } + if (typeof multiplier != "undefined") { + this.multipliers.push(multiplier); + } + if (typeof limit != "undefined") { + this.limits.push(limit); + } + this.update(); + } + + /** + * Remove a modifier + */ + removeModifier(cumulative?: number, multiplier?: number, limit?: number): void { + if (typeof cumulative != "undefined") { + remove(this.cumulatives, cumulative); + } + if (typeof multiplier != "undefined") { + remove(this.multipliers, multiplier); + } + if (typeof limit != "undefined") { + remove(this.limits, limit); + } + this.update(); + } + + /** + * Update the current value + */ + private update(): void { + let value = sum(this.cumulatives); + if (this.multipliers.length) { + value = Math.round(value * (1 + sum(this.multipliers) / 100)); + } + if (this.limits.length) { + value = Math.min(value, min(this.limits)); + } + this.current = value; + } +} + +/** + * Set of ShipAttribute for a ship + */ +export class ShipAttributes { + // Initiative (capacity to play first) + initiative = new ShipAttribute() + // Maximal hull value + hull_capacity = new ShipAttribute() + // Maximal shield value + shield_capacity = new ShipAttribute() + // Damage evasion + evasion = new ShipAttribute() + // Maximal power value + power_capacity = new ShipAttribute() +} + +/** + * Set of simple values for a ship + */ +export class ShipValues { + hull = 0 + shield = 0 + power = 0 +} + +/** + * Static attributes and values object for property queries + */ +export const SHIP_ATTRIBUTES = new ShipAttributes(); +export const SHIP_VALUES = new ShipValues(); + +/** + * Type guards + */ +export function isShipValue(key: string): key is keyof ShipValues { + return SHIP_VALUES.hasOwnProperty(key); +} +export function isShipAttribute(key: string): key is keyof ShipAttributes { + return SHIP_ATTRIBUTES.hasOwnProperty(key); } diff --git a/src/core/Shop.spec.ts b/src/core/Shop.spec.ts index b773b8d..1a44a5f 100644 --- a/src/core/Shop.spec.ts +++ b/src/core/Shop.spec.ts @@ -1,39 +1,44 @@ -module TK.SpaceTac.Specs { - testing("Shop", test => { - test.case("generates secondary missions", check => { - let universe = new Universe(); - universe.generate(4); - let start = universe.getStartLocation(); +import { testing } from "../common/Testing"; +import { Mission } from "./missions/Mission"; +import { Player } from "./Player"; +import { Shop } from "./Shop"; +import { StarLocation } from "./StarLocation"; +import { Universe } from "./Universe"; - let shop = new Shop(); - check.equals((shop).missions.length, 0); +testing("Shop", test => { + test.case("generates secondary missions", check => { + let universe = new Universe(); + universe.generate(4); + let start = universe.getStartLocation(); - let result = shop.getMissions(start, 4); - check.equals(result.length, 4); - check.equals((shop).missions.length, 4); + let shop = new Shop(); + check.equals((shop).missions.length, 0); - let oresult = shop.getMissions(start, 4); - check.equals(oresult, result); + let result = shop.getMissions(start, 4); + check.equals(result.length, 4); + check.equals((shop).missions.length, 4); - result.forEach(mission => { - check.equals(mission.main, false); - }); - }); + let oresult = shop.getMissions(start, 4); + check.equals(oresult, result); - test.case("assigns missions to a fleet", check => { - let shop = new Shop(); - let player = new Player(); - let mission = new Mission(new Universe()); - (shop).missions = [mission]; - - check.equals(shop.getMissions(new StarLocation(), 1), [mission]); - check.equals(player.missions.secondary, []); - - shop.acceptMission(mission, player); - - check.equals((shop).missions, []); - check.equals(player.missions.secondary, [mission]); - check.same(mission.fleet, player.fleet); - }); + result.forEach(mission => { + check.equals(mission.main, false); }); -} + }); + + test.case("assigns missions to a fleet", check => { + let shop = new Shop(); + let player = new Player(); + let mission = new Mission(new Universe()); + (shop).missions = [mission]; + + check.equals(shop.getMissions(new StarLocation(), 1), [mission]); + check.equals(player.missions.secondary, []); + + shop.acceptMission(mission, player); + + check.equals((shop).missions, []); + check.equals(player.missions.secondary, [mission]); + check.same(mission.fleet, player.fleet); + }); +}); diff --git a/src/core/Shop.ts b/src/core/Shop.ts index b515876..6819bbd 100644 --- a/src/core/Shop.ts +++ b/src/core/Shop.ts @@ -1,51 +1,56 @@ -module TK.SpaceTac { - /** - * A shop is a place to buy/sell equipments - */ - export class Shop { - // Average level of equipment - private level: number +import { RandomGenerator } from "../common/RandomGenerator"; +import { contains, remove } from "../common/Tools"; +import { Mission } from "./missions/Mission"; +import { MissionGenerator } from "./missions/MissionGenerator"; +import { Player } from "./Player"; +import { StarLocation } from "./StarLocation"; - // Random generator - private random: RandomGenerator +/** + * A shop is a place to buy/sell equipments + */ +export class Shop { + // Average level of equipment + private level: number - // Available missions - private missions: Mission[] = [] + // Random generator + private random: RandomGenerator - constructor(level = 1) { - this.level = level; - this.random = new RandomGenerator(); - } + // Available missions + private missions: Mission[] = [] - /** - * Get a list of available secondary missions - */ - getMissions(around: StarLocation, max_count = 3): Mission[] { - while (this.missions.length < max_count) { - let generator = new MissionGenerator(around.star.universe, around, this.random); - let mission = generator.generate(); - this.missions.push(mission); - } + constructor(level = 1) { + this.level = level; + this.random = new RandomGenerator(); + } - return this.missions; - } - - /** - * Assign a mission to a fleet - * - * Returns true on success - */ - acceptMission(mission: Mission, player: Player): boolean { - if (contains(this.missions, mission)) { - if (player.missions.addSecondary(mission, player.fleet)) { - remove(this.missions, mission); - return true; - } else { - return false; - } - } else { - return false; - } - } + /** + * Get a list of available secondary missions + */ + getMissions(around: StarLocation, max_count = 3): Mission[] { + while (this.missions.length < max_count) { + let generator = new MissionGenerator(around.star.universe, around, this.random); + let mission = generator.generate(); + this.missions.push(mission); } -} \ No newline at end of file + + return this.missions; + } + + /** + * Assign a mission to a fleet + * + * Returns true on success + */ + acceptMission(mission: Mission, player: Player): boolean { + if (contains(this.missions, mission)) { + if (player.missions.addSecondary(mission, player.fleet)) { + remove(this.missions, mission); + return true; + } else { + return false; + } + } else { + return false; + } + } +} diff --git a/src/core/Star.spec.ts b/src/core/Star.spec.ts index d180ee7..1cd49b4 100644 --- a/src/core/Star.spec.ts +++ b/src/core/Star.spec.ts @@ -1,36 +1,39 @@ -module TK.SpaceTac.Specs { - testing("Star", test => { - test.case("lists links to other stars", check => { - var universe = new Universe(); - universe.stars.push(new Star(universe, 0, 0, "Star A")); - universe.stars.push(new Star(universe, 1, 0, "Star B")); - universe.stars.push(new Star(universe, 0, 1, "Star C")); - universe.stars.push(new Star(universe, 1, 1, "Star D")); - universe.addLink(universe.stars[0], universe.stars[1]); - universe.addLink(universe.stars[0], universe.stars[3]); +import { testing } from "../common/Testing"; +import { Star } from "./Star"; +import { StarLink } from "./StarLink"; +import { Universe } from "./Universe"; - var result = universe.stars[0].getLinks(); - check.equals(result.length, 2); - check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1])); - check.equals(result[1], new StarLink(universe.stars[0], universe.stars[3])); +testing("Star", test => { + test.case("lists links to other stars", check => { + var universe = new Universe(); + universe.stars.push(new Star(universe, 0, 0, "Star A")); + universe.stars.push(new Star(universe, 1, 0, "Star B")); + universe.stars.push(new Star(universe, 0, 1, "Star C")); + universe.stars.push(new Star(universe, 1, 1, "Star D")); + universe.addLink(universe.stars[0], universe.stars[1]); + universe.addLink(universe.stars[0], universe.stars[3]); - check.equals(universe.stars[0].getLinkTo(universe.stars[1]), universe.starlinks[0]); - check.equals(universe.stars[0].getLinkTo(universe.stars[2]), null); - check.equals(universe.stars[0].getLinkTo(universe.stars[3]), universe.starlinks[1]); - check.equals(universe.stars[1].getLinkTo(universe.stars[0]), universe.starlinks[0]); - check.equals(universe.stars[1].getLinkTo(universe.stars[2]), null); - check.equals(universe.stars[1].getLinkTo(universe.stars[3]), null); - check.equals(universe.stars[2].getLinkTo(universe.stars[0]), null); - check.equals(universe.stars[2].getLinkTo(universe.stars[1]), null); - check.equals(universe.stars[2].getLinkTo(universe.stars[3]), null); - check.equals(universe.stars[3].getLinkTo(universe.stars[0]), universe.starlinks[1]); - check.equals(universe.stars[3].getLinkTo(universe.stars[1]), null); - check.equals(universe.stars[3].getLinkTo(universe.stars[2]), null); + var result = universe.stars[0].getLinks(); + check.equals(result.length, 2); + check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1])); + check.equals(result[1], new StarLink(universe.stars[0], universe.stars[3])); - let neighbors = universe.stars[0].getNeighbors(); - check.equals(neighbors.length, 2); - check.contains(neighbors, universe.stars[1]); - check.contains(neighbors, universe.stars[3]); - }); - }); -} + check.equals(universe.stars[0].getLinkTo(universe.stars[1]), universe.starlinks[0]); + check.equals(universe.stars[0].getLinkTo(universe.stars[2]), null); + check.equals(universe.stars[0].getLinkTo(universe.stars[3]), universe.starlinks[1]); + check.equals(universe.stars[1].getLinkTo(universe.stars[0]), universe.starlinks[0]); + check.equals(universe.stars[1].getLinkTo(universe.stars[2]), null); + check.equals(universe.stars[1].getLinkTo(universe.stars[3]), null); + check.equals(universe.stars[2].getLinkTo(universe.stars[0]), null); + check.equals(universe.stars[2].getLinkTo(universe.stars[1]), null); + check.equals(universe.stars[2].getLinkTo(universe.stars[3]), null); + check.equals(universe.stars[3].getLinkTo(universe.stars[0]), universe.starlinks[1]); + check.equals(universe.stars[3].getLinkTo(universe.stars[1]), null); + check.equals(universe.stars[3].getLinkTo(universe.stars[2]), null); + + let neighbors = universe.stars[0].getNeighbors(); + check.equals(neighbors.length, 2); + check.contains(neighbors, universe.stars[1]); + check.contains(neighbors, universe.stars[3]); + }); +}); diff --git a/src/core/Star.ts b/src/core/Star.ts index 3aa396a..9c8ce25 100644 --- a/src/core/Star.ts +++ b/src/core/Star.ts @@ -1,198 +1,202 @@ -module TK.SpaceTac { - // A star system - export class Star { +import { RandomGenerator } from "../common/RandomGenerator"; +import { nna } from "../common/Tools"; +import { StarLink } from "./StarLink"; +import { StarLocation, StarLocationType } from "./StarLocation"; +import { Universe } from "./Universe"; - // Available names for star systems - static NAMES_POOL = [ - "Alpha Prime", - "Bright Skies", - "Costan Sector", - "Duncan's Legacy", - "Ethiopea", - "Fringe Space", - "Gesurd Deep", - "Helios", - "Justice Enclave", - "Kovak Second", - "Lumen Circle", - "Manoa Society", - "Neptune's Record", - "Ominous Murmur", - "Pirate's Landing", - "Quasuc Effect", - "Roaring Thunder", - "Safe Passage", - "Time Holes", - "Unknown Territory", - "Vulcan Terror", - "Wings Aurora", - "Xenos Trading", - "Yu's Pride", - "Zoki's Hammer", - "Astral Tempest", - "Burned Star", - "Crystal Bride", - "Death Star", - "Ether Bending", - "Forgotten Realm", - "Galactic Ring", - "Hegemonia", - "Jorgon Trails", - "Kemini System", - "Light Rain", - "Moons Astride", - "Nubia's Sisters", - "Opium Hide", - "Paradise Quest", - "Quarter Horizon", - "Rising Dust", - "Silence of Forge", - "Titan Feet", - "Unicorn Fly", - "Violated Sanctuary", - "World's Repose", - "Xanthia's Travel", - "Yggdrasil", - "Zone of Ending", - ]; +// A star system +export class Star { - // Parent universe - universe: Universe; + // Available names for star systems + static NAMES_POOL = [ + "Alpha Prime", + "Bright Skies", + "Costan Sector", + "Duncan's Legacy", + "Ethiopea", + "Fringe Space", + "Gesurd Deep", + "Helios", + "Justice Enclave", + "Kovak Second", + "Lumen Circle", + "Manoa Society", + "Neptune's Record", + "Ominous Murmur", + "Pirate's Landing", + "Quasuc Effect", + "Roaring Thunder", + "Safe Passage", + "Time Holes", + "Unknown Territory", + "Vulcan Terror", + "Wings Aurora", + "Xenos Trading", + "Yu's Pride", + "Zoki's Hammer", + "Astral Tempest", + "Burned Star", + "Crystal Bride", + "Death Star", + "Ether Bending", + "Forgotten Realm", + "Galactic Ring", + "Hegemonia", + "Jorgon Trails", + "Kemini System", + "Light Rain", + "Moons Astride", + "Nubia's Sisters", + "Opium Hide", + "Paradise Quest", + "Quarter Horizon", + "Rising Dust", + "Silence of Forge", + "Titan Feet", + "Unicorn Fly", + "Violated Sanctuary", + "World's Repose", + "Xanthia's Travel", + "Yggdrasil", + "Zone of Ending", + ]; - // Name of the system (unique in the universe) - name: string; + // Parent universe + universe: Universe; - // Location in the universe - x: number; - y: number; + // Name of the system (unique in the universe) + name: string; - // Radius of the star system - radius: number; + // Location in the universe + x: number; + y: number; - // List of points of interest - locations: StarLocation[]; + // Radius of the star system + radius: number; - // Base level for encounters in this system - level: number; + // List of points of interest + locations: StarLocation[]; - constructor(universe: Universe | null = null, x = 0, y = 0, name = "") { - this.universe = universe || new Universe(); - this.x = x; - this.y = y; - this.radius = 0.1; - this.locations = [new StarLocation(this, StarLocationType.STAR, 0, 0)]; - this.level = 1; - this.name = name; - } + // Base level for encounters in this system + level: number; - jasmineToString(): string { - return `Star ${this.name}`; - } + constructor(universe: Universe | null = null, x = 0, y = 0, name = "") { + this.universe = universe || new Universe(); + this.x = x; + this.y = y; + this.radius = 0.1; + this.locations = [new StarLocation(this, StarLocationType.STAR, 0, 0)]; + this.level = 1; + this.name = name; + } - /** - * Add a location of interest - */ - addLocation(type: StarLocationType): StarLocation { - let result = new StarLocation(this, type); - this.locations.push(result); - return result; - } + jasmineToString(): string { + return `Star ${this.name}`; + } - // Get the distance to another star - getDistanceTo(star: Star): number { - var dx = this.x - star.x; - var dy = this.y - star.y; + /** + * Add a location of interest + */ + addLocation(type: StarLocationType): StarLocation { + let result = new StarLocation(this, type); + this.locations.push(result); + return result; + } - return Math.sqrt(dx * dx + dy * dy); - } + // Get the distance to another star + getDistanceTo(star: Star): number { + var dx = this.x - star.x; + var dy = this.y - star.y; - // Generate the contents of this star system - generate(random = RandomGenerator.global): void { - var location_count = random.randInt(2 + Math.floor(this.level / 2), 3 + this.level); - if (this.name.length == 0) { - this.name = random.choice(Star.NAMES_POOL); - } - this.generateLocations(location_count, random); - } + return Math.sqrt(dx * dx + dy * dy); + } - // Generate points of interest (*count* doesn't include the star and warp locations) - generateLocations(count: number, random = RandomGenerator.global): void { - while (count--) { - this.generateOneLocation(StarLocationType.PLANET, this.locations, this.radius * 0.2, this.radius * 0.6, random); - } - } - - // Generate a warp location to another star (to be bound later) - generateWarpLocationTo(other: Star, random = RandomGenerator.global): StarLocation { - let fav_phi = Math.atan2(other.y - this.y, other.x - this.x); - var warp = this.generateOneLocation(StarLocationType.WARP, this.locations, this.radius * 0.75, this.radius * 0.85, random, fav_phi); - return warp; - } - - // Get all direct links to other stars - getLinks(all = this.universe.starlinks): StarLink[] { - var result: StarLink[] = []; - - all.forEach(link => { - if (link.first === this || link.second === this) { - result.push(link); - } - }); - - return result; - } - - // Get the link to another star, null if not found - getLinkTo(other: Star, all = this.universe.starlinks): StarLink | null { - var result: StarLink | null = null; - - all.forEach(link => { - if (link.isLinking(this, other)) { - result = link; - } - }); - - return result; - } - - // Get the warp location to another star, null if not found - getWarpLocationTo(other: Star): StarLocation | null { - var result: StarLocation | null = null; - - this.locations.forEach(location => { - if (location.type == StarLocationType.WARP && location.jump_dest && location.jump_dest.star == other) { - result = location; - } - }); - - return result; - } - - /** - * Get the neighboring star systems (single jump accessible) - */ - getNeighbors(all = this.universe.starlinks): Star[] { - return nna(this.getLinks(all).map(link => link.getPeer(this))); - } - - // Check if a location is far enough from all other ones - private checkMinDistance(loc: StarLocation, others: StarLocation[]): boolean { - return others.every((iloc: StarLocation): boolean => { - return iloc.getDistanceTo(loc) > this.radius * 0.15; - }); - } - - // Generate a single location - private generateOneLocation(type: StarLocationType, others: StarLocation[], radius_min: number, radius_max: number, random: RandomGenerator, fav_phi: number | null = null): StarLocation { - do { - var phi = fav_phi ? (fav_phi + random.random() * 0.4 - 0.2) : (random.random() * Math.PI * 2); - var r = random.random() * (radius_max - radius_min) + radius_min; - var result = new StarLocation(this, type, r * Math.cos(phi), r * Math.sin(phi)); - } while (!this.checkMinDistance(result, others)); - - this.locations.push(result); - - return result; - } + // Generate the contents of this star system + generate(random = RandomGenerator.global): void { + var location_count = random.randInt(2 + Math.floor(this.level / 2), 3 + this.level); + if (this.name.length == 0) { + this.name = random.choice(Star.NAMES_POOL); } + this.generateLocations(location_count, random); + } + + // Generate points of interest (*count* doesn't include the star and warp locations) + generateLocations(count: number, random = RandomGenerator.global): void { + while (count--) { + this.generateOneLocation(StarLocationType.PLANET, this.locations, this.radius * 0.2, this.radius * 0.6, random); + } + } + + // Generate a warp location to another star (to be bound later) + generateWarpLocationTo(other: Star, random = RandomGenerator.global): StarLocation { + let fav_phi = Math.atan2(other.y - this.y, other.x - this.x); + var warp = this.generateOneLocation(StarLocationType.WARP, this.locations, this.radius * 0.75, this.radius * 0.85, random, fav_phi); + return warp; + } + + // Get all direct links to other stars + getLinks(all = this.universe.starlinks): StarLink[] { + var result: StarLink[] = []; + + all.forEach(link => { + if (link.first === this || link.second === this) { + result.push(link); + } + }); + + return result; + } + + // Get the link to another star, null if not found + getLinkTo(other: Star, all = this.universe.starlinks): StarLink | null { + var result: StarLink | null = null; + + all.forEach(link => { + if (link.isLinking(this, other)) { + result = link; + } + }); + + return result; + } + + // Get the warp location to another star, null if not found + getWarpLocationTo(other: Star): StarLocation | null { + var result: StarLocation | null = null; + + this.locations.forEach(location => { + if (location.type == StarLocationType.WARP && location.jump_dest && location.jump_dest.star == other) { + result = location; + } + }); + + return result; + } + + /** + * Get the neighboring star systems (single jump accessible) + */ + getNeighbors(all = this.universe.starlinks): Star[] { + return nna(this.getLinks(all).map(link => link.getPeer(this))); + } + + // Check if a location is far enough from all other ones + private checkMinDistance(loc: StarLocation, others: StarLocation[]): boolean { + return others.every((iloc: StarLocation): boolean => { + return iloc.getDistanceTo(loc) > this.radius * 0.15; + }); + } + + // Generate a single location + private generateOneLocation(type: StarLocationType, others: StarLocation[], radius_min: number, radius_max: number, random: RandomGenerator, fav_phi: number | null = null): StarLocation { + do { + var phi = fav_phi ? (fav_phi + random.random() * 0.4 - 0.2) : (random.random() * Math.PI * 2); + var r = random.random() * (radius_max - radius_min) + radius_min; + var result = new StarLocation(this, type, r * Math.cos(phi), r * Math.sin(phi)); + } while (!this.checkMinDistance(result, others)); + + this.locations.push(result); + + return result; + } } diff --git a/src/core/StarLink.spec.ts b/src/core/StarLink.spec.ts index 1a8db49..1196597 100644 --- a/src/core/StarLink.spec.ts +++ b/src/core/StarLink.spec.ts @@ -1,38 +1,40 @@ -module TK.SpaceTac.Specs { - testing("StarLink", test => { - test.case("checks link intersection", check => { - var star1 = new Star(null, 0, 0); - var star2 = new Star(null, 0, 1); - var star3 = new Star(null, 1, 0); - var star4 = new Star(null, 1, 1); - var link1 = new StarLink(star1, star2); - var link2 = new StarLink(star1, star3); - var link3 = new StarLink(star1, star4); - var link4 = new StarLink(star2, star3); - var link5 = new StarLink(star2, star4); - var link6 = new StarLink(star3, star4); - var links = [link1, link2, link3, link4, link5, link6]; - links.forEach((first: StarLink) => { - links.forEach((second: StarLink) => { - if (first !== second) { - var expected = (first === link3 && second === link4) || - (first === link4 && second === link3); - check.same(first.isCrossing(second), expected); - check.same(second.isCrossing(first), expected); - } - }); - }); - }); +import { testing } from "../common/Testing"; +import { Star } from "./Star"; +import { StarLink } from "./StarLink"; - test.case("gets the peer of a given sector", check => { - var star1 = new Star(null, 0, 0); - var star2 = new Star(null, 0, 1); - var star3 = new Star(null, 0, 1); - var link1 = new StarLink(star1, star2); - - check.same(link1.getPeer(star1), star2); - check.same(link1.getPeer(star2), star1); - check.equals(link1.getPeer(star3), null); - }); +testing("StarLink", test => { + test.case("checks link intersection", check => { + var star1 = new Star(null, 0, 0); + var star2 = new Star(null, 0, 1); + var star3 = new Star(null, 1, 0); + var star4 = new Star(null, 1, 1); + var link1 = new StarLink(star1, star2); + var link2 = new StarLink(star1, star3); + var link3 = new StarLink(star1, star4); + var link4 = new StarLink(star2, star3); + var link5 = new StarLink(star2, star4); + var link6 = new StarLink(star3, star4); + var links = [link1, link2, link3, link4, link5, link6]; + links.forEach((first: StarLink) => { + links.forEach((second: StarLink) => { + if (first !== second) { + var expected = (first === link3 && second === link4) || + (first === link4 && second === link3); + check.same(first.isCrossing(second), expected); + check.same(second.isCrossing(first), expected); + } + }); }); -} + }); + + test.case("gets the peer of a given sector", check => { + var star1 = new Star(null, 0, 0); + var star2 = new Star(null, 0, 1); + var star3 = new Star(null, 0, 1); + var link1 = new StarLink(star1, star2); + + check.same(link1.getPeer(star1), star2); + check.same(link1.getPeer(star2), star1); + check.equals(link1.getPeer(star3), null); + }); +}); diff --git a/src/core/StarLink.ts b/src/core/StarLink.ts index 57cf90a..65d41f5 100644 --- a/src/core/StarLink.ts +++ b/src/core/StarLink.ts @@ -1,49 +1,49 @@ -module TK.SpaceTac { - // An hyperspace link between two star systems - export class StarLink { - // Stars - first: Star; - second: Star; +import { Star } from "./Star"; - constructor(first: Star, second: Star) { - this.first = first; - this.second = second; - } +// An hyperspace link between two star systems +export class StarLink { + // Stars + first: Star; + second: Star; - // Check if this links bounds the two stars together, in either way - isLinking(first: Star, second: Star) { - return (this.first === first && this.second === second) || (this.first === second && this.second === first); - } + constructor(first: Star, second: Star) { + this.first = first; + this.second = second; + } - // Get the length of a link - getLength(): number { - return this.first.getDistanceTo(this.second); - } + // Check if this links bounds the two stars together, in either way + isLinking(first: Star, second: Star) { + return (this.first === first && this.second === second) || (this.first === second && this.second === first); + } - // Check if this link crosses another - isCrossing(other: StarLink): boolean { - if (this.first === other.first || this.second === other.first || this.first === other.second || this.second === other.second) { - return false; - } - var ccw = (a: Star, b: Star, c: Star): boolean => { - return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); - }; - var cc1 = ccw(this.first, other.first, other.second); - var cc2 = ccw(this.second, other.first, other.second); - var cc3 = ccw(this.first, this.second, other.first); - var cc4 = ccw(this.first, this.second, other.second); - return cc1 !== cc2 && cc3 !== cc4; - } + // Get the length of a link + getLength(): number { + return this.first.getDistanceTo(this.second); + } - // Get the other side of the link, for a given side - getPeer(star: Star): Star | null { - if (star === this.first) { - return this.second; - } else if (star === this.second) { - return this.first; - } else { - return null; - } - } + // Check if this link crosses another + isCrossing(other: StarLink): boolean { + if (this.first === other.first || this.second === other.first || this.first === other.second || this.second === other.second) { + return false; } + var ccw = (a: Star, b: Star, c: Star): boolean => { + return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); + }; + var cc1 = ccw(this.first, other.first, other.second); + var cc2 = ccw(this.second, other.first, other.second); + var cc3 = ccw(this.first, this.second, other.first); + var cc4 = ccw(this.first, this.second, other.second); + return cc1 !== cc2 && cc3 !== cc4; + } + + // Get the other side of the link, for a given side + getPeer(star: Star): Star | null { + if (star === this.first) { + return this.second; + } else if (star === this.second) { + return this.first; + } else { + return null; + } + } } diff --git a/src/core/StarLocation.spec.ts b/src/core/StarLocation.spec.ts index 675ce56..2121165 100644 --- a/src/core/StarLocation.spec.ts +++ b/src/core/StarLocation.spec.ts @@ -1,37 +1,41 @@ -module TK.SpaceTac.Specs { - testing("StarLocation", test => { - test.case("removes generated encounters that lose", check => { - var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0); - var fleet = new Fleet(); - fleet.addShip(); - location.encounter_random = new SkewedRandomGenerator([0]); - var battle = nn(location.enterLocation(fleet)); +import { SkewedRandomGenerator } from "../common/RandomGenerator"; +import { testing } from "../common/Testing"; +import { nn } from "../common/Tools"; +import { Fleet } from "./Fleet"; +import { StarLocation, StarLocationType } from "./StarLocation"; - check.notequals(location.encounter, null); - check.notequals(battle, null); +testing("StarLocation", test => { + test.case("removes generated encounters that lose", check => { + var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0); + var fleet = new Fleet(); + fleet.addShip(); + location.encounter_random = new SkewedRandomGenerator([0]); + var battle = nn(location.enterLocation(fleet)); - battle.endBattle(fleet); - check.notequals(location.encounter, null); + check.notequals(location.encounter, null); + check.notequals(battle, null); - location.resolveEncounter(nn(battle.outcome)); - check.equals(location.encounter, null); - }); + battle.endBattle(fleet); + check.notequals(location.encounter, null); - test.case("leaves generated encounters that win", check => { - var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0); - var fleet = new Fleet(); - fleet.addShip(); - location.encounter_random = new SkewedRandomGenerator([0]); - var battle = nn(location.enterLocation(fleet)); + location.resolveEncounter(nn(battle.outcome)); + check.equals(location.encounter, null); + }); - check.notequals(location.encounter, null); - check.notequals(battle, null); + test.case("leaves generated encounters that win", check => { + var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0); + var fleet = new Fleet(); + fleet.addShip(); + location.encounter_random = new SkewedRandomGenerator([0]); + var battle = nn(location.enterLocation(fleet)); - battle.endBattle(location.encounter); - check.notequals(location.encounter, null); + check.notequals(location.encounter, null); + check.notequals(battle, null); - location.resolveEncounter(nn(battle.outcome)); - check.notequals(location.encounter, null); - }); - }); -} + battle.endBattle(location.encounter); + check.notequals(location.encounter, null); + + location.resolveEncounter(nn(battle.outcome)); + check.notequals(location.encounter, null); + }); +}); diff --git a/src/core/StarLocation.ts b/src/core/StarLocation.ts index a100ca2..627772c 100644 --- a/src/core/StarLocation.ts +++ b/src/core/StarLocation.ts @@ -1,181 +1,189 @@ -/// +import { RandomGenerator } from "../common/RandomGenerator" +import { RObject } from "../common/RObject" +import { add, remove } from "../common/Tools" +import { Battle } from "./Battle" +import { BattleOutcome } from "./BattleOutcome" +import { Fleet } from "./Fleet" +import { FleetGenerator } from "./FleetGenerator" +import { Player } from "./Player" +import { Shop } from "./Shop" +import { Star } from "./Star" +import { Universe } from "./Universe" -module TK.SpaceTac { - export enum StarLocationType { - STAR, - WARP, - PLANET, - ASTEROID, - STATION - } - - /** - * Point of interest in a star system - */ - export class StarLocation extends RObject { - // Parent star system - star: Star - - // Type of location - type: StarLocationType - - // Location in the star system - x: number - y: number - - // Absolute location in the universe - universe_x: number - universe_y: number - - // Destination for jump, if its a WARP location - jump_dest: StarLocation | null - - // Fleets present at this location (excluding the encounter for now) - fleets: Fleet[] = [] - - // Enemy encounter - encounter: Fleet | null = null - encounter_gen = false - encounter_random = RandomGenerator.global - - // Shop to buy/sell equipment - shop: Shop | null = null - - constructor(star = new Star(), type: StarLocationType = StarLocationType.PLANET, x: number = 0, y: number = 0) { - super(); - - this.star = star; - this.type = type; - this.x = x; - this.y = y; - this.universe_x = this.star.x + this.x; - this.universe_y = this.star.y + this.y; - this.jump_dest = null; - } - - /** - * Get the universe containing this location - */ - get universe(): Universe { - return this.star.universe; - } - - /** - * Add a shop in this location - */ - addShop(level = this.star.level) { - this.shop = new Shop(level); - } - - /** - * Remove a potential shop in this location - */ - removeShop(): void { - this.shop = null; - } - - /** - * Add a fleet to the list of fleets present in this system - */ - addFleet(fleet: Fleet): void { - if (add(this.fleets, fleet)) { - this.enterLocation(fleet); - } - } - - /** - * Remove a fleet from the list of fleets present in this system - */ - removeFleet(fleet: Fleet): void { - remove(this.fleets, fleet); - } - - /** - * Check if the location is clear of encounter - */ - isClear(): boolean { - return this.encounter_gen && this.encounter === null; - } - - // Set the jump destination of a WARP location - setJumpDestination(jump_dest: StarLocation): void { - if (this.type === StarLocationType.WARP) { - this.jump_dest = jump_dest; - } - } - - // Call this when first probing a location to generate the possible encounter - // Returns the encountered fleet, null if no encounter happens - tryGenerateEncounter(): Fleet | null { - if (!this.encounter_gen) { - this.encounter_gen = true; - - if (this.encounter_random.random() < 0.8) { - this.setupEncounter(); - } - } - - return this.encounter; - } - - // Call this when entering a location to generate the possible encounter - // *fleet* is the player fleet, entering the location - // Returns the engaged battle, null if no encounter happens - enterLocation(fleet: Fleet): Battle | null { - let encounter = this.tryGenerateEncounter(); - if (encounter) { - let battle = new Battle(fleet, encounter); - battle.start(); - return battle; - } else { - return null; - } - } - - // Get the distance to another location - getDistanceTo(other: StarLocation): number { - var dx = this.x - other.x; - var dy = this.y - other.y; - - return Math.sqrt(dx * dx + dy * dy); - } - - /** - * Clear an encounter, when the encountered fleet has been defeated - */ - clearEncounter() { - this.encounter_gen = true; - this.encounter = null; - } - - /** - * Forces the setup of an encounter - */ - setupEncounter() { - this.encounter_gen = true; - - let fleet_generator = new FleetGenerator(this.encounter_random); - let variations: [number, number][]; - if (this.star.level == 1) { - variations = [[this.star.level, 2]]; - } else if (this.star.level <= 3) { - variations = [[this.star.level, 2], [this.star.level - 1, 3]]; - } else if (this.star.level <= 6) { - variations = [[this.star.level, 3], [this.star.level - 1, 4], [this.star.level + 1, 2]]; - } else { - variations = [[this.star.level, 4], [this.star.level - 1, 5], [this.star.level + 1, 3], [this.star.level + 3, 2]]; - } - let [level, enemies] = this.encounter_random.choice(variations); - this.encounter = fleet_generator.generate(level, new Player("Enemy"), enemies, true); - } - - /** - * Resolves the encounter from a battle outcome - */ - resolveEncounter(outcome: BattleOutcome) { - if (this.encounter && outcome.winner && !this.encounter.is(outcome.winner)) { - this.clearEncounter(); - } - } - } +export enum StarLocationType { + STAR, + WARP, + PLANET, + ASTEROID, + STATION +} + +/** + * Point of interest in a star system + */ +export class StarLocation extends RObject { + // Parent star system + star: Star + + // Type of location + type: StarLocationType + + // Location in the star system + x: number + y: number + + // Absolute location in the universe + universe_x: number + universe_y: number + + // Destination for jump, if its a WARP location + jump_dest: StarLocation | null + + // Fleets present at this location (excluding the encounter for now) + fleets: Fleet[] = [] + + // Enemy encounter + encounter: Fleet | null = null + encounter_gen = false + encounter_random = RandomGenerator.global + + // Shop to buy/sell equipment + shop: Shop | null = null + + constructor(star = new Star(), type: StarLocationType = StarLocationType.PLANET, x: number = 0, y: number = 0) { + super(); + + this.star = star; + this.type = type; + this.x = x; + this.y = y; + this.universe_x = this.star.x + this.x; + this.universe_y = this.star.y + this.y; + this.jump_dest = null; + } + + /** + * Get the universe containing this location + */ + get universe(): Universe { + return this.star.universe; + } + + /** + * Add a shop in this location + */ + addShop(level = this.star.level) { + this.shop = new Shop(level); + } + + /** + * Remove a potential shop in this location + */ + removeShop(): void { + this.shop = null; + } + + /** + * Add a fleet to the list of fleets present in this system + */ + addFleet(fleet: Fleet): void { + if (add(this.fleets, fleet)) { + this.enterLocation(fleet); + } + } + + /** + * Remove a fleet from the list of fleets present in this system + */ + removeFleet(fleet: Fleet): void { + remove(this.fleets, fleet); + } + + /** + * Check if the location is clear of encounter + */ + isClear(): boolean { + return this.encounter_gen && this.encounter === null; + } + + // Set the jump destination of a WARP location + setJumpDestination(jump_dest: StarLocation): void { + if (this.type === StarLocationType.WARP) { + this.jump_dest = jump_dest; + } + } + + // Call this when first probing a location to generate the possible encounter + // Returns the encountered fleet, null if no encounter happens + tryGenerateEncounter(): Fleet | null { + if (!this.encounter_gen) { + this.encounter_gen = true; + + if (this.encounter_random.random() < 0.8) { + this.setupEncounter(); + } + } + + return this.encounter; + } + + // Call this when entering a location to generate the possible encounter + // *fleet* is the player fleet, entering the location + // Returns the engaged battle, null if no encounter happens + enterLocation(fleet: Fleet): Battle | null { + let encounter = this.tryGenerateEncounter(); + if (encounter) { + let battle = new Battle(fleet, encounter); + battle.start(); + return battle; + } else { + return null; + } + } + + // Get the distance to another location + getDistanceTo(other: StarLocation): number { + var dx = this.x - other.x; + var dy = this.y - other.y; + + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * Clear an encounter, when the encountered fleet has been defeated + */ + clearEncounter() { + this.encounter_gen = true; + this.encounter = null; + } + + /** + * Forces the setup of an encounter + */ + setupEncounter() { + this.encounter_gen = true; + + let fleet_generator = new FleetGenerator(this.encounter_random); + let variations: [number, number][]; + if (this.star.level == 1) { + variations = [[this.star.level, 2]]; + } else if (this.star.level <= 3) { + variations = [[this.star.level, 2], [this.star.level - 1, 3]]; + } else if (this.star.level <= 6) { + variations = [[this.star.level, 3], [this.star.level - 1, 4], [this.star.level + 1, 2]]; + } else { + variations = [[this.star.level, 4], [this.star.level - 1, 5], [this.star.level + 1, 3], [this.star.level + 3, 2]]; + } + let [level, enemies] = this.encounter_random.choice(variations); + this.encounter = fleet_generator.generate(level, new Player("Enemy"), enemies, true); + } + + /** + * Resolves the encounter from a battle outcome + */ + resolveEncounter(outcome: BattleOutcome) { + if (this.encounter && outcome.winner && !this.encounter.is(outcome.winner)) { + this.clearEncounter(); + } + } } diff --git a/src/core/Target.spec.ts b/src/core/Target.spec.ts index 21ed974..eeba579 100644 --- a/src/core/Target.spec.ts +++ b/src/core/Target.spec.ts @@ -1,62 +1,64 @@ -module TK.SpaceTac.Specs { - testing("Target", test => { - test.case("initializes from ship or location", check => { - var target: Target; +import { testing } from "../common/Testing"; +import { Ship } from "./Ship"; +import { Target } from "./Target"; - target = Target.newFromLocation(2, 3); - check.equals(target.x, 2); - check.equals(target.y, 3); - check.equals(target.ship_id, null); +testing("Target", test => { + test.case("initializes from ship or location", check => { + var target: Target; - var ship = new Ship(); - ship.arena_x = 4; - ship.arena_y = -2.1; - target = Target.newFromShip(ship); - check.equals(target.x, 4); - check.equals(target.y, -2.1); - check.equals(target.ship_id, ship.id); - }); + target = Target.newFromLocation(2, 3); + check.equals(target.x, 2); + check.equals(target.y, 3); + check.equals(target.ship_id, null); - test.case("gets distance to another target", check => { - var t1 = Target.newFromLocation(5, 1); - var t2 = Target.newFromLocation(6, 2); - check.nears(t1.getDistanceTo(t2), Math.sqrt(2)); - }); + var ship = new Ship(); + ship.arena_x = 4; + ship.arena_y = -2.1; + target = Target.newFromShip(ship); + check.equals(target.x, 4); + check.equals(target.y, -2.1); + check.equals(target.ship_id, ship.id); + }); - test.case("gets angle to another target", check => { - var t1 = Target.newFromLocation(2, 3); - var t2 = Target.newFromLocation(4, 5); - check.nears(t1.getAngleTo(t2), Math.PI / 4); - }); + test.case("gets distance to another target", check => { + var t1 = Target.newFromLocation(5, 1); + var t2 = Target.newFromLocation(6, 2); + check.nears(t1.getDistanceTo(t2), Math.sqrt(2)); + }); - test.case("checks if a target is in range of another", check => { - var t1 = Target.newFromLocation(5, 4); - check.equals(t1.isInRange(7, 3, 2), false); - check.equals(t1.isInRange(7, 3, 3), true); - check.equals(t1.isInRange(5, 5, 2), true); - }); + test.case("gets angle to another target", check => { + var t1 = Target.newFromLocation(2, 3); + var t2 = Target.newFromLocation(4, 5); + check.nears(t1.getAngleTo(t2), Math.PI / 4); + }); - test.case("constraints a target to a limited range", check => { - var target = Target.newFromLocation(5, 9); - check.equals(target.constraintInRange(1, 1, Math.sqrt(80) * 0.5), Target.newFromLocation(3, 5)); - check.same(target.constraintInRange(1, 1, 70), target); - }); + test.case("checks if a target is in range of another", check => { + var t1 = Target.newFromLocation(5, 4); + check.equals(t1.isInRange(7, 3, 2), false); + check.equals(t1.isInRange(7, 3, 3), true); + check.equals(t1.isInRange(5, 5, 2), true); + }); - test.case("pushes a target out of a given circle", check => { - var target = Target.newFromLocation(5, 5); - check.same(target.moveOutOfCircle(0, 0, 3, 0, 0), target); - check.equals(target.moveOutOfCircle(6, 6, 3, 0, 0), Target.newFromLocation(3.8786796564403576, 3.8786796564403576)); - check.equals(target.moveOutOfCircle(4, 4, 3, 10, 10), Target.newFromLocation(6.121320343559642, 6.121320343559642)); - check.equals(target.moveOutOfCircle(5, 8, 6, 5, 0), Target.newFromLocation(5, 2)); - check.equals(target.moveOutOfCircle(5, 2, 6, 5, 10), Target.newFromLocation(5, 8)); - check.equals(target.moveOutOfCircle(8, 5, 6, 0, 5), Target.newFromLocation(2, 5)); - check.equals(target.moveOutOfCircle(2, 5, 6, 10, 5), Target.newFromLocation(8, 5)); - }); + test.case("constraints a target to a limited range", check => { + var target = Target.newFromLocation(5, 9); + check.equals(target.constraintInRange(1, 1, Math.sqrt(80) * 0.5), Target.newFromLocation(3, 5)); + check.same(target.constraintInRange(1, 1, 70), target); + }); - test.case("keeps a target inside a rectangle", check => { - var target = Target.newFromLocation(5, 5); - check.same(target.keepInsideRectangle(0, 0, 10, 10, 0, 0), target); - check.equals(target.keepInsideRectangle(8, 0, 13, 10, 10, 5), Target.newFromLocation(8, 5)); - }); - }); -} + test.case("pushes a target out of a given circle", check => { + var target = Target.newFromLocation(5, 5); + check.same(target.moveOutOfCircle(0, 0, 3, 0, 0), target); + check.equals(target.moveOutOfCircle(6, 6, 3, 0, 0), Target.newFromLocation(3.8786796564403576, 3.8786796564403576)); + check.equals(target.moveOutOfCircle(4, 4, 3, 10, 10), Target.newFromLocation(6.121320343559642, 6.121320343559642)); + check.equals(target.moveOutOfCircle(5, 8, 6, 5, 0), Target.newFromLocation(5, 2)); + check.equals(target.moveOutOfCircle(5, 2, 6, 5, 10), Target.newFromLocation(5, 8)); + check.equals(target.moveOutOfCircle(8, 5, 6, 0, 5), Target.newFromLocation(2, 5)); + check.equals(target.moveOutOfCircle(2, 5, 6, 10, 5), Target.newFromLocation(8, 5)); + }); + + test.case("keeps a target inside a rectangle", check => { + var target = Target.newFromLocation(5, 5); + check.same(target.keepInsideRectangle(0, 0, 10, 10, 0, 0), target); + check.equals(target.keepInsideRectangle(8, 0, 13, 10, 10, 5), Target.newFromLocation(8, 5)); + }); +}); diff --git a/src/core/Target.ts b/src/core/Target.ts index 75a12ba..13b07ab 100644 --- a/src/core/Target.ts +++ b/src/core/Target.ts @@ -1,186 +1,190 @@ -module TK.SpaceTac { - // Find the nearest intersection between a line and a circle - // Circle is supposed to be centered at (0,0) - // Nearest intersection to (x1,y1) is returned - function intersectLineCircle(x1: number, y1: number, x2: number, y2: number, r: number): [number, number] | null { - let a = y2 - y1; - let b = -(x2 - x1); - let c = -(a * x1 + b * y1); - let x0 = -a * c / (a * a + b * b), y0 = -b * c / (a * a + b * b); - let EPS = 10e-8; - if (c * c > r * r * (a * a + b * b) + EPS) { - return null; - } else if (Math.abs(c * c - r * r * (a * a + b * b)) < EPS) { - return [x0, y0]; - } else { - let d = r * r - c * c / (a * a + b * b); - let mult = Math.sqrt(d / (a * a + b * b)); - let ax, ay, bx, by; - ax = x0 + b * mult; - bx = x0 - b * mult; - ay = y0 - a * mult; - by = y0 + a * mult; +import { RObjectId } from "../common/RObject"; +import { minBy } from "../common/Tools"; +import { IArenaGrid } from "./ArenaGrid"; +import { Battle } from "./Battle"; +import { Ship } from "./Ship"; - let candidates: [number, number][] = [ - [x0 + b * mult, y0 - a * mult], - [x0 - b * mult, y0 + a * mult] - ] - return minBy(candidates, ([x, y]) => Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1))); - } - } +// Find the nearest intersection between a line and a circle +// Circle is supposed to be centered at (0,0) +// Nearest intersection to (x1,y1) is returned +function intersectLineCircle(x1: number, y1: number, x2: number, y2: number, r: number): [number, number] | null { + let a = y2 - y1; + let b = -(x2 - x1); + let c = -(a * x1 + b * y1); + let x0 = -a * c / (a * a + b * b), y0 = -b * c / (a * a + b * b); + let EPS = 10e-8; + if (c * c > r * r * (a * a + b * b) + EPS) { + return null; + } else if (Math.abs(c * c - r * r * (a * a + b * b)) < EPS) { + return [x0, y0]; + } else { + let d = r * r - c * c / (a * a + b * b); + let mult = Math.sqrt(d / (a * a + b * b)); + let ax, ay, bx, by; + ax = x0 + b * mult; + bx = x0 - b * mult; + ay = y0 - a * mult; + by = y0 + a * mult; - // Target for a capability - // This could be a location in space, or a ship - export class Target { - // Coordinates of the target - x: number - y: number - - // If the target is a ship, this attribute will be set - ship_id: RObjectId | null - - // Standard constructor - constructor(x: number, y: number, ship: Ship | null = null) { - this.x = x; - this.y = y; - this.ship_id = ship ? ship.id : null; - } - - jasmineToString() { - if (this.ship_id) { - return `(${this.x},${this.y}) ship_id=${this.ship_id}}`; - } else { - return `(${this.x},${this.y})`; - } - } - - // Constructor to target a single ship - static newFromShip(ship: Ship): Target { - return new Target(ship.arena_x, ship.arena_y, ship); - } - - // Constructor to target a location in space - static newFromLocation(x: number, y: number): Target { - return new Target(x, y, null); - } - - /** - * Snap to battle grid - */ - snap(grid: IArenaGrid): Target { - if (this.ship_id) { - return this; - } else { - let location = grid.snap(this); - return Target.newFromLocation(location.x, location.y); - } - } - - // Get distance to another target - getDistanceTo(other: { x: number, y: number }): number { - var dx = other.x - this.x; - var dy = other.y - this.y; - return Math.sqrt(dx * dx + dy * dy); - } - - // Get the normalized angle, in radians, to another target - getAngleTo(other: { x: number, y: number }): number { - var dx = other.x - this.x; - var dy = other.y - this.y; - return Math.atan2(dy, dx); - } - - /** - * Returns true if the target is a ship - */ - isShip(): boolean { - return this.ship_id !== null; - } - - /** - * Get the targetted ship in a battle - */ - getShip(battle: Battle): Ship | null { - if (this.isShip()) { - return battle.getShip(this.ship_id); - } else { - return null; - } - } - - // Check if a target is in range from a specific point - isInRange(x: number, y: number, radius: number): boolean { - var dx = this.x - x; - var dy = this.y - y; - var length = Math.sqrt(dx * dx + dy * dy); - return (length <= radius); - } - - // Constraint a target, to be in a given range from a specific point - // May return the original target if it's already in radius - constraintInRange(x: number, y: number, radius: number): Target { - var dx = this.x - x; - var dy = this.y - y; - var length = Math.sqrt(dx * dx + dy * dy); - if (length <= radius) { - return this; - } else { - var factor = radius / length; - return Target.newFromLocation(x + dx * factor, y + dy * factor); - } - } - - // Force a target to stay out of a given circle - // If the target is in the circle, it will be moved to the nearest intersection between targetting line - // and the circle - // May return the original target if it's already out of the circle - moveOutOfCircle(circlex: number, circley: number, radius: number, sourcex: number, sourcey: number): Target { - var dx = this.x - circlex; - var dy = this.y - circley; - var length = Math.sqrt(dx * dx + dy * dy); - if (length >= radius) { - // Already out of circle - return this; - } else { - // Find nearest intersection with circle - var res = intersectLineCircle(sourcex - circlex, sourcey - circley, dx, dy, radius); - if (res) { - return Target.newFromLocation(res[0] + circlex, res[1] + circley); - } else { - return this; - } - } - } - - /** - * Keep the target inside a rectangle - * - * May return the original target if it's already inside the rectangle - */ - keepInsideRectangle(xmin: number, ymin: number, xmax: number, ymax: number, sourcex: number, sourcey: number): Target { - let length = this.getDistanceTo({ x: sourcex, y: sourcey }); - let result: Target = this; - if (result.x < xmin) { - let factor = (xmin - sourcex) / (result.x - sourcex); - length *= factor; - result = result.constraintInRange(sourcex, sourcey, length); - } - if (result.x > xmax) { - let factor = (xmax - sourcex) / (result.x - sourcex); - length *= factor; - result = result.constraintInRange(sourcex, sourcey, length); - } - if (result.y < ymin) { - let factor = (ymin - sourcey) / (result.y - sourcey); - length *= factor; - result = result.constraintInRange(sourcex, sourcey, length); - } - if (result.y > ymax) { - let factor = (ymax - sourcey) / (result.y - sourcey); - length *= factor; - result = result.constraintInRange(sourcex, sourcey, length); - } - return result; - } - } + let candidates: [number, number][] = [ + [x0 + b * mult, y0 - a * mult], + [x0 - b * mult, y0 + a * mult] + ] + return minBy(candidates, ([x, y]) => Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1))); + } +} + +// Target for a capability +// This could be a location in space, or a ship +export class Target { + // Coordinates of the target + x: number + y: number + + // If the target is a ship, this attribute will be set + ship_id: RObjectId | null + + // Standard constructor + constructor(x: number, y: number, ship: Ship | null = null) { + this.x = x; + this.y = y; + this.ship_id = ship ? ship.id : null; + } + + jasmineToString() { + if (this.ship_id) { + return `(${this.x},${this.y}) ship_id=${this.ship_id}}`; + } else { + return `(${this.x},${this.y})`; + } + } + + // Constructor to target a single ship + static newFromShip(ship: Ship): Target { + return new Target(ship.arena_x, ship.arena_y, ship); + } + + // Constructor to target a location in space + static newFromLocation(x: number, y: number): Target { + return new Target(x, y, null); + } + + /** + * Snap to battle grid + */ + snap(grid: IArenaGrid): Target { + if (this.ship_id) { + return this; + } else { + let location = grid.snap(this); + return Target.newFromLocation(location.x, location.y); + } + } + + // Get distance to another target + getDistanceTo(other: { x: number, y: number }): number { + var dx = other.x - this.x; + var dy = other.y - this.y; + return Math.sqrt(dx * dx + dy * dy); + } + + // Get the normalized angle, in radians, to another target + getAngleTo(other: { x: number, y: number }): number { + var dx = other.x - this.x; + var dy = other.y - this.y; + return Math.atan2(dy, dx); + } + + /** + * Returns true if the target is a ship + */ + isShip(): boolean { + return this.ship_id !== null; + } + + /** + * Get the targetted ship in a battle + */ + getShip(battle: Battle): Ship | null { + if (this.isShip()) { + return battle.getShip(this.ship_id); + } else { + return null; + } + } + + // Check if a target is in range from a specific point + isInRange(x: number, y: number, radius: number): boolean { + var dx = this.x - x; + var dy = this.y - y; + var length = Math.sqrt(dx * dx + dy * dy); + return (length <= radius); + } + + // Constraint a target, to be in a given range from a specific point + // May return the original target if it's already in radius + constraintInRange(x: number, y: number, radius: number): Target { + var dx = this.x - x; + var dy = this.y - y; + var length = Math.sqrt(dx * dx + dy * dy); + if (length <= radius) { + return this; + } else { + var factor = radius / length; + return Target.newFromLocation(x + dx * factor, y + dy * factor); + } + } + + // Force a target to stay out of a given circle + // If the target is in the circle, it will be moved to the nearest intersection between targetting line + // and the circle + // May return the original target if it's already out of the circle + moveOutOfCircle(circlex: number, circley: number, radius: number, sourcex: number, sourcey: number): Target { + var dx = this.x - circlex; + var dy = this.y - circley; + var length = Math.sqrt(dx * dx + dy * dy); + if (length >= radius) { + // Already out of circle + return this; + } else { + // Find nearest intersection with circle + var res = intersectLineCircle(sourcex - circlex, sourcey - circley, dx, dy, radius); + if (res) { + return Target.newFromLocation(res[0] + circlex, res[1] + circley); + } else { + return this; + } + } + } + + /** + * Keep the target inside a rectangle + * + * May return the original target if it's already inside the rectangle + */ + keepInsideRectangle(xmin: number, ymin: number, xmax: number, ymax: number, sourcex: number, sourcey: number): Target { + let length = this.getDistanceTo({ x: sourcex, y: sourcey }); + let result: Target = this; + if (result.x < xmin) { + let factor = (xmin - sourcex) / (result.x - sourcex); + length *= factor; + result = result.constraintInRange(sourcex, sourcey, length); + } + if (result.x > xmax) { + let factor = (xmax - sourcex) / (result.x - sourcex); + length *= factor; + result = result.constraintInRange(sourcex, sourcey, length); + } + if (result.y < ymin) { + let factor = (ymin - sourcey) / (result.y - sourcey); + length *= factor; + result = result.constraintInRange(sourcex, sourcey, length); + } + if (result.y > ymax) { + let factor = (ymax - sourcey) / (result.y - sourcey); + length *= factor; + result = result.constraintInRange(sourcex, sourcey, length); + } + return result; + } } diff --git a/src/core/TestTools.spec.ts b/src/core/TestTools.spec.ts index 21b6680..2937184 100644 --- a/src/core/TestTools.spec.ts +++ b/src/core/TestTools.spec.ts @@ -1,23 +1,25 @@ -module TK.SpaceTac.Specs { - testing("TestTools", test => { - test.case("set ship health and power", check => { - let ship = new Ship(); +import { testing } from "../common/Testing"; +import { Ship } from "./Ship"; +import { TestTools } from "./TestTools"; - check.equals(ship.getAttribute("hull_capacity"), 0); - check.equals(ship.getAttribute("shield_capacity"), 0); - check.equals(ship.getAttribute("power_capacity"), 0); - check.equals(ship.getValue("hull"), 0); - check.equals(ship.getValue("shield"), 0); - check.equals(ship.getValue("power"), 0); +testing("TestTools", test => { + test.case("set ship health and power", check => { + let ship = new Ship(); - TestTools.setShipModel(ship, 100, 200, 12); + check.equals(ship.getAttribute("hull_capacity"), 0); + check.equals(ship.getAttribute("shield_capacity"), 0); + check.equals(ship.getAttribute("power_capacity"), 0); + check.equals(ship.getValue("hull"), 0); + check.equals(ship.getValue("shield"), 0); + check.equals(ship.getValue("power"), 0); - check.equals(ship.getAttribute("hull_capacity"), 100); - check.equals(ship.getAttribute("shield_capacity"), 200); - check.equals(ship.getAttribute("power_capacity"), 12); - check.equals(ship.getValue("hull"), 100); - check.equals(ship.getValue("shield"), 200); - check.equals(ship.getValue("power"), 12); - }); - }); -} \ No newline at end of file + TestTools.setShipModel(ship, 100, 200, 12); + + check.equals(ship.getAttribute("hull_capacity"), 100); + check.equals(ship.getAttribute("shield_capacity"), 200); + check.equals(ship.getAttribute("power_capacity"), 12); + check.equals(ship.getValue("hull"), 100); + check.equals(ship.getValue("shield"), 200); + check.equals(ship.getValue("power"), 12); + }); +}); diff --git a/src/core/TestTools.ts b/src/core/TestTools.ts index ee7e7af..9d189aa 100644 --- a/src/core/TestTools.ts +++ b/src/core/TestTools.ts @@ -1,188 +1,206 @@ -module TK.SpaceTac { - // unit testing utilities - export class TestTools { +import { RObject } from "../common/RObject"; +import { TestContext } from "../common/Testing"; +import { add, copyfields } from "../common/Tools"; +import { BaseAction } from "./actions/BaseAction"; +import { DeployDroneAction } from "./actions/DeployDroneAction"; +import { MoveAction } from "./actions/MoveAction"; +import { ToggleAction } from "./actions/ToggleAction"; +import { Battle } from "./Battle"; +import { BaseBattleDiff } from "./diffs/BaseBattleDiff"; +import { AttributeEffect } from "./effects/AttributeEffect"; +import { BaseEffect } from "./effects/BaseEffect"; +import { DamageEffect, DamageEffectMode } from "./effects/DamageEffect"; +import { StickyEffect } from "./effects/StickyEffect"; +import { Fleet } from "./Fleet"; +import { ShipModel } from "./models/ShipModel"; +import { Player } from "./Player"; +import { Ship } from "./Ship"; +import { ShipAttributes } from "./ShipValue"; +import { Target } from "./Target"; - // Create a battle between two fleets, with a fixed play order (owned ships, then enemy ships) - static createBattle(own_ships = 1, enemy_ships = 1): Battle { - var fleet1 = new Fleet(new Player("Attacker")); - var fleet2 = new Fleet(new Player("Defender")); +// unit testing utilities +export class TestTools { - while (own_ships--) { - fleet1.addShip(); - } - while (enemy_ships--) { - fleet2.addShip(); - } + // Create a battle between two fleets, with a fixed play order (owned ships, then enemy ships) + static createBattle(own_ships = 1, enemy_ships = 1): Battle { + var fleet1 = new Fleet(new Player("Attacker")); + var fleet2 = new Fleet(new Player("Defender")); - var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0)); - battle.play_order = fleet1.ships.concat(fleet2.ships); - battle.setPlayingShip(battle.play_order[0]); - return battle; - } - - /** - * Add an engine, allowing a ship to move *distance*, for each action points - */ - static addEngine(ship: Ship, distance: number): MoveAction { - let action = new MoveAction("Engine", { distance_per_power: distance }); - ship.actions.addCustom(action); - return action; - } - - /** - * Add a weapon to a ship - */ - static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0, angle = 0): TriggerAction { - let action = new TriggerAction("Weapon", { - effects: [new DamageEffect(damage, DamageEffectMode.SHIELD_THEN_HULL)], - power: power_usage, - range: max_distance, - blast: blast, - angle: angle, - }); - ship.actions.addCustom(action); - return action; - } - - /** - * Force the current playing ship - */ - static setShipPlaying(battle: Battle, ship: Ship): void { - add(battle.play_order, ship); - battle.play_index = battle.play_order.indexOf(ship); - ship.playing = true; - } - - /** - * Set a ship attributes (by changing its model) - */ - static setShipModel(ship: Ship, hull: number, shield = 0, power = 0, level = 1, actions: BaseAction[] = [], effects: BaseEffect[] = []) { - let model = new ShipModel(); - ship.level.forceLevel(level); - ship.model = model; - - // TODO Use a BaseModel subclass would be prettier - model.getActions = () => actions; - model.getEffects = () => effects.concat([ - new AttributeEffect("hull_capacity", hull), - new AttributeEffect("shield_capacity", shield), - new AttributeEffect("power_capacity", power), - ]); - - ship.actions.updateFromShip(ship); - - ship.updateAttributes(); - ship.restoreHealth(); - ship.setValue("power", power); - } - - /** - * Force a ship attribute to a given value - * - * Beware that a call to ship.updateAttributes() may cancel this - */ - static setAttribute(ship: Ship, name: keyof ShipAttributes, value: number): void { - let attr = ship.attributes[name]; - attr.reset(); - attr.addModifier(value); - } - - /** - * Check a diff chain on a given battle - * - * This will apply all diffs, then reverts them, checking at each step the battle state - */ - static diffChain(check: TestContext, battle: Battle, diffs: BaseBattleDiff[], checks: ((check: TestContext) => void)[]): void { - checks[0](check.sub("initial state")); - - for (let i = 0; i < diffs.length; i++) { - diffs[i].apply(battle); - checks[i + 1](check.sub(`after diff ${i + 1} applied`)); - } - - for (let i = diffs.length - 1; i >= 0; i--) { - diffs[i].revert(battle); - checks[i](check.sub(`after diff ${i + 1} reverted`)); - } - } - - /** - * Check an action chain on a given battle - * - * This will apply all actions, then reverts them, checking at each step the battle state - */ - static actionChain(check: TestContext, battle: Battle, actions: [Ship, BaseAction, Target | undefined][], checks: ((check: TestContext) => void)[]): void { - checks[0](check.sub("initial state")); - - for (let i = 0; i < actions.length; i++) { - let [ship, action, target] = actions[i]; - battle.setPlayingShip(ship); - let result = battle.applyOneAction(action.id, target); - check.equals(result, true, `action ${i + 1} successfully applied`); - checks[i + 1](check.sub(`after action ${i + 1} applied`)); - } - - for (let i = actions.length - 1; i >= 0; i--) { - battle.revertOneAction(); - checks[i](check.sub(`after action ${i + 1} reverted`)); - } - } + while (own_ships--) { + fleet1.addShip(); + } + while (enemy_ships--) { + fleet2.addShip(); } - function strip(obj: T, attr: keyof T): any { - let result: any = {}; - copyfields(obj, result); - delete result[attr]; - return result; + var battle = new Battle(fleet1, fleet2); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0)); + battle.play_order = fleet1.ships.concat(fleet2.ships); + battle.setPlayingShip(battle.play_order[0]); + return battle; + } + + /** + * Add an engine, allowing a ship to move *distance*, for each action points + */ + static addEngine(ship: Ship, distance: number): MoveAction { + let action = new MoveAction("Engine", { distance_per_power: distance }); + ship.actions.addCustom(action); + return action; + } + + /** + * Add a weapon to a ship + */ + static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0, angle = 0): TriggerAction { + let action = new TriggerAction("Weapon", { + effects: [new DamageEffect(damage, DamageEffectMode.SHIELD_THEN_HULL)], + power: power_usage, + range: max_distance, + blast: blast, + angle: angle, + }); + ship.actions.addCustom(action); + return action; + } + + /** + * Force the current playing ship + */ + static setShipPlaying(battle: Battle, ship: Ship): void { + add(battle.play_order, ship); + battle.play_index = battle.play_order.indexOf(ship); + ship.playing = true; + } + + /** + * Set a ship attributes (by changing its model) + */ + static setShipModel(ship: Ship, hull: number, shield = 0, power = 0, level = 1, actions: BaseAction[] = [], effects: BaseEffect[] = []) { + let model = new ShipModel(); + ship.level.forceLevel(level); + ship.model = model; + + // TODO Use a BaseModel subclass would be prettier + model.getActions = () => actions; + model.getEffects = () => effects.concat([ + new AttributeEffect("hull_capacity", hull), + new AttributeEffect("shield_capacity", shield), + new AttributeEffect("power_capacity", power), + ]); + + ship.actions.updateFromShip(ship); + + ship.updateAttributes(); + ship.restoreHealth(); + ship.setValue("power", power); + } + + /** + * Force a ship attribute to a given value + * + * Beware that a call to ship.updateAttributes() may cancel this + */ + static setAttribute(ship: Ship, name: keyof ShipAttributes, value: number): void { + let attr = ship.attributes[name]; + attr.reset(); + attr.addModifier(value); + } + + /** + * Check a diff chain on a given battle + * + * This will apply all diffs, then reverts them, checking at each step the battle state + */ + static diffChain(check: TestContext, battle: Battle, diffs: BaseBattleDiff[], checks: ((check: TestContext) => void)[]): void { + checks[0](check.sub("initial state")); + + for (let i = 0; i < diffs.length; i++) { + diffs[i].apply(battle); + checks[i + 1](check.sub(`after diff ${i + 1} applied`)); } - function strip_id(effect: RObject): any { - if (effect instanceof StickyEffect) { - let result = strip(effect, "id"); - result.base = strip_id(result.base); - return result; - } else { - return strip(effect, "id"); - } + for (let i = diffs.length - 1; i >= 0; i--) { + diffs[i].revert(battle); + checks[i](check.sub(`after diff ${i + 1} reverted`)); + } + } + + /** + * Check an action chain on a given battle + * + * This will apply all actions, then reverts them, checking at each step the battle state + */ + static actionChain(check: TestContext, battle: Battle, actions: [Ship, BaseAction, Target | undefined][], checks: ((check: TestContext) => void)[]): void { + checks[0](check.sub("initial state")); + + for (let i = 0; i < actions.length; i++) { + let [ship, action, target] = actions[i]; + battle.setPlayingShip(ship); + let result = battle.applyOneAction(action.id, target); + check.equals(result, true, `action ${i + 1} successfully applied`); + checks[i + 1](check.sub(`after action ${i + 1} applied`)); } - export function compare_effects(check: TestContext, effects1: BaseEffect[], effects2: BaseEffect[]): void { - check.equals(effects1.map(strip_id), effects2.map(strip_id), "effects"); - } - - export function compare_action(check: TestContext, action1: BaseAction | null, action2: BaseAction | null): void { - if (action1 === null || action2 === null) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(action1), strip_id(action2), "action"); - } - } - - export function compare_trigger_action(check: TestContext, action1: BaseAction | null, action2: TriggerAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof TriggerAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); - compare_effects(check, action1.effects, action2.effects); - } - } - - export function compare_toggle_action(check: TestContext, action1: BaseAction | null, action2: ToggleAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof ToggleAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); - compare_effects(check, action1.effects, action2.effects); - } - } - - export function compare_drone_action(check: TestContext, action1: BaseAction | null, action2: DeployDroneAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof DeployDroneAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "drone_effects")), strip_id(strip(action2, "drone_effects")), "action"); - compare_effects(check, action1.drone_effects, action2.drone_effects); - } + for (let i = actions.length - 1; i >= 0; i--) { + battle.revertOneAction(); + checks[i](check.sub(`after action ${i + 1} reverted`)); } + } +} + +function strip(obj: T, attr: keyof T): any { + let result: any = {}; + copyfields(obj, result); + delete result[attr]; + return result; +} + +function strip_id(effect: RObject): any { + if (effect instanceof StickyEffect) { + let result = strip(effect, "id"); + result.base = strip_id(result.base); + return result; + } else { + return strip(effect, "id"); + } +} + +export function compare_effects(check: TestContext, effects1: BaseEffect[], effects2: BaseEffect[]): void { + check.equals(effects1.map(strip_id), effects2.map(strip_id), "effects"); +} + +export function compare_action(check: TestContext, action1: BaseAction | null, action2: BaseAction | null): void { + if (action1 === null || action2 === null) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(action1), strip_id(action2), "action"); + } +} + +export function compare_trigger_action(check: TestContext, action1: BaseAction | null, action2: TriggerAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof TriggerAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); + compare_effects(check, action1.effects, action2.effects); + } +} + +export function compare_toggle_action(check: TestContext, action1: BaseAction | null, action2: ToggleAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof ToggleAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); + compare_effects(check, action1.effects, action2.effects); + } +} + +export function compare_drone_action(check: TestContext, action1: BaseAction | null, action2: DeployDroneAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof DeployDroneAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "drone_effects")), strip_id(strip(action2, "drone_effects")), "action"); + compare_effects(check, action1.drone_effects, action2.drone_effects); + } } diff --git a/src/core/Universe.spec.ts b/src/core/Universe.spec.ts index 9386cc9..fc065dc 100644 --- a/src/core/Universe.spec.ts +++ b/src/core/Universe.spec.ts @@ -1,132 +1,137 @@ -module TK.SpaceTac.Specs { - testing("Universe", test => { - test.case("generates star systems", check => { - var universe = new Universe(); - var result = universe.generateStars(31); +import { testing } from "../common/Testing"; +import { any, cmp, nn } from "../common/Tools"; +import { Star } from "./Star"; +import { StarLink } from "./StarLink"; +import { StarLocation, StarLocationType } from "./StarLocation"; +import { Universe } from "./Universe"; - check.equals(result.length, 31); - }); +testing("Universe", test => { + test.case("generates star systems", check => { + var universe = new Universe(); + var result = universe.generateStars(31); - test.case("lists potential links between star systems", check => { - var universe = new Universe(); - universe.stars.push(new Star(universe, 0, 0)); - universe.stars.push(new Star(universe, 0, 1)); - universe.stars.push(new Star(universe, 1, 0)); + check.equals(result.length, 31); + }); - var result = universe.getPotentialLinks(); - check.equals(result.length, 3); - check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1])); - check.equals(result[1], new StarLink(universe.stars[0], universe.stars[2])); - check.equals(result[2], new StarLink(universe.stars[1], universe.stars[2])); - }); + test.case("lists potential links between star systems", check => { + var universe = new Universe(); + universe.stars.push(new Star(universe, 0, 0)); + universe.stars.push(new Star(universe, 0, 1)); + universe.stars.push(new Star(universe, 1, 0)); - test.case("filters out crossing links", check => { - var universe = new Universe(); - universe.stars.push(new Star(universe, 0, 0)); - universe.stars.push(new Star(universe, 0, 1)); - universe.stars.push(new Star(universe, 1, 0)); - universe.stars.push(new Star(universe, 2, 2)); + var result = universe.getPotentialLinks(); + check.equals(result.length, 3); + check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1])); + check.equals(result[1], new StarLink(universe.stars[0], universe.stars[2])); + check.equals(result[2], new StarLink(universe.stars[1], universe.stars[2])); + }); - var result = universe.getPotentialLinks(); - check.equals(result.length, 6); + test.case("filters out crossing links", check => { + var universe = new Universe(); + universe.stars.push(new Star(universe, 0, 0)); + universe.stars.push(new Star(universe, 0, 1)); + universe.stars.push(new Star(universe, 1, 0)); + universe.stars.push(new Star(universe, 2, 2)); - var filtered = universe.filterCrossingLinks(result); - check.equals(filtered.length, 5); - check.equals(any(filtered, link => link.isLinking(universe.stars[1], universe.stars[2])), true); - check.equals(any(filtered, link => link.isLinking(universe.stars[0], universe.stars[3])), false); - }); + var result = universe.getPotentialLinks(); + check.equals(result.length, 6); - test.case("filters out redundant links", check => { - let universe = new Universe(); - let s1 = universe.addStar(1, "S1", 0, 0); - let s2 = universe.addStar(1, "S2", 0, 1); - let s3 = universe.addStar(1, "S3", 1, 0); - let s4 = universe.addStar(1, "S4", 0.75, 0.75); + var filtered = universe.filterCrossingLinks(result); + check.equals(filtered.length, 5); + check.equals(any(filtered, link => link.isLinking(universe.stars[1], universe.stars[2])), true); + check.equals(any(filtered, link => link.isLinking(universe.stars[0], universe.stars[3])), false); + }); - let links = [ - new StarLink(s1, s2), - new StarLink(s1, s3), - new StarLink(s2, s3), - new StarLink(s2, s4), - new StarLink(s3, s4), - ] + test.case("filters out redundant links", check => { + let universe = new Universe(); + let s1 = universe.addStar(1, "S1", 0, 0); + let s2 = universe.addStar(1, "S2", 0, 1); + let s3 = universe.addStar(1, "S3", 1, 0); + let s4 = universe.addStar(1, "S4", 0.75, 0.75); - let filtered = universe.filterRedundantLinks(links); - check.equals(filtered.length, 4); - check.contains(filtered, links[0]); - check.notcontains(filtered, links[2]); - }); + let links = [ + new StarLink(s1, s2), + new StarLink(s1, s3), + new StarLink(s2, s3), + new StarLink(s2, s4), + new StarLink(s3, s4), + ] - test.case("generates warp locations", check => { - var universe = new Universe(); - universe.stars.push(new Star(universe, 0, 0, "0")); - universe.stars.push(new Star(universe, 1, 0, "1")); - universe.stars.push(new Star(universe, 1, 1, "2")); - universe.addLink(universe.stars[0], universe.stars[1]); - universe.addLink(universe.stars[0], universe.stars[2]); + let filtered = universe.filterRedundantLinks(links); + check.equals(filtered.length, 4); + check.contains(filtered, links[0]); + check.notcontains(filtered, links[2]); + }); - var getWarps = (star: Star): StarLocation[] => { - var result: StarLocation[] = []; - star.locations.forEach((location: StarLocation) => { - if (location.type === StarLocationType.WARP) { - result.push(location); - } - }); - return result; - }; + test.case("generates warp locations", check => { + var universe = new Universe(); + universe.stars.push(new Star(universe, 0, 0, "0")); + universe.stars.push(new Star(universe, 1, 0, "1")); + universe.stars.push(new Star(universe, 1, 1, "2")); + universe.addLink(universe.stars[0], universe.stars[1]); + universe.addLink(universe.stars[0], universe.stars[2]); - check.equals(getWarps(universe.stars[0]).length, 0); - check.equals(getWarps(universe.stars[1]).length, 0); - check.equals(getWarps(universe.stars[2]).length, 0); + var getWarps = (star: Star): StarLocation[] => { + var result: StarLocation[] = []; + star.locations.forEach((location: StarLocation) => { + if (location.type === StarLocationType.WARP) { + result.push(location); + } + }); + return result; + }; - universe.generateWarpLocations(); + check.equals(getWarps(universe.stars[0]).length, 0); + check.equals(getWarps(universe.stars[1]).length, 0); + check.equals(getWarps(universe.stars[2]).length, 0); - var warps = getWarps(universe.stars[0]); - check.equals(warps.length, 2); - check.same(nn(warps[0].jump_dest).star, universe.stars[1]); - check.same(nn(warps[1].jump_dest).star, universe.stars[2]); - check.same(universe.stars[0].getWarpLocationTo(universe.stars[1]), warps[0]); - check.same(universe.stars[0].getWarpLocationTo(universe.stars[2]), warps[1]); - warps = getWarps(universe.stars[1]); - check.equals(warps.length, 1); - check.same(nn(warps[0].jump_dest).star, universe.stars[0]); - check.equals(universe.stars[1].getWarpLocationTo(universe.stars[2]), null); - warps = getWarps(universe.stars[2]); - check.equals(warps.length, 1); - check.same(nn(warps[0].jump_dest).star, universe.stars[0]); - }); + universe.generateWarpLocations(); - test.case("generates danger gradients", check => { - let universe = new Universe(); + var warps = getWarps(universe.stars[0]); + check.equals(warps.length, 2); + check.same(nn(warps[0].jump_dest).star, universe.stars[1]); + check.same(nn(warps[1].jump_dest).star, universe.stars[2]); + check.same(universe.stars[0].getWarpLocationTo(universe.stars[1]), warps[0]); + check.same(universe.stars[0].getWarpLocationTo(universe.stars[2]), warps[1]); + warps = getWarps(universe.stars[1]); + check.equals(warps.length, 1); + check.same(nn(warps[0].jump_dest).star, universe.stars[0]); + check.equals(universe.stars[1].getWarpLocationTo(universe.stars[2]), null); + warps = getWarps(universe.stars[2]); + check.equals(warps.length, 1); + check.same(nn(warps[0].jump_dest).star, universe.stars[0]); + }); - universe.stars.push(new Star(universe)); - universe.stars.push(new Star(universe)); - universe.stars.push(new Star(universe)); - universe.stars.push(new Star(universe)); + test.case("generates danger gradients", check => { + let universe = new Universe(); - universe.addLink(universe.stars[0], universe.stars[1]); - universe.addLink(universe.stars[0], universe.stars[2]); - universe.addLink(universe.stars[3], universe.stars[1]); - universe.addLink(universe.stars[3], universe.stars[2]); + universe.stars.push(new Star(universe)); + universe.stars.push(new Star(universe)); + universe.stars.push(new Star(universe)); + universe.stars.push(new Star(universe)); - universe.setEncounterLevels(9); - check.equals(universe.stars.map(star => star.level).sort(cmp), [1, 5, 5, 9]); - }); + universe.addLink(universe.stars[0], universe.stars[1]); + universe.addLink(universe.stars[0], universe.stars[2]); + universe.addLink(universe.stars[3], universe.stars[1]); + universe.addLink(universe.stars[3], universe.stars[2]); - test.case("gets a good start location", check => { - let universe = new Universe(); + universe.setEncounterLevels(9); + check.equals(universe.stars.map(star => star.level).sort(cmp), [1, 5, 5, 9]); + }); - universe.stars.push(new Star(universe)); - universe.stars.push(new Star(universe)); - universe.stars.push(new Star(universe)); + test.case("gets a good start location", check => { + let universe = new Universe(); - universe.stars[0].level = 8; - universe.stars[1].level = 1; - universe.stars[2].level = 4; + universe.stars.push(new Star(universe)); + universe.stars.push(new Star(universe)); + universe.stars.push(new Star(universe)); - universe.stars[1].generateLocations(5); + universe.stars[0].level = 8; + universe.stars[1].level = 1; + universe.stars[2].level = 4; - check.same(universe.getStartLocation(), universe.stars[1].locations[0]); - }); - }); -} + universe.stars[1].generateLocations(5); + + check.same(universe.getStartLocation(), universe.stars[1].locations[0]); + }); +}); diff --git a/src/core/Universe.ts b/src/core/Universe.ts index c637012..43b7127 100644 --- a/src/core/Universe.ts +++ b/src/core/Universe.ts @@ -1,278 +1,284 @@ -module TK.SpaceTac { - /** - * Main game universe - */ - export class Universe { - // List of star systems - stars: Star[] = [] +import { RandomGenerator } from "../common/RandomGenerator" +import { RObjectContainer, RObjectId } from "../common/RObject" +import { any, flatten, intersection, max, min, minBy, nn, range, sortedBy } from "../common/Tools" +import { NameGenerator } from "./NameGenerator" +import { Star } from "./Star" +import { StarLink } from "./StarLink" +import { StarLocation } from "./StarLocation" - // List of links between star systems - starlinks: StarLink[] = [] +/** + * Main game universe + */ +export class Universe { + // List of star systems + stars: Star[] = [] - // Collection of all star locations - locations = new RObjectContainer() + // List of links between star systems + starlinks: StarLink[] = [] - // Radius of the universe - radius = 5 + // Collection of all star locations + locations = new RObjectContainer() - // Source of randomness - random = RandomGenerator.global + // Radius of the universe + radius = 5 - /** - * Add a single star - */ - addStar(level = 1, name?: string, x = 0, y = 0): Star { - let result = new Star(this, x, y, name || `Star ${this.stars.length + 1}`); - result.level = level; - this.stars.push(result); - return result; - } + // Source of randomness + random = RandomGenerator.global - /** - * Update the locations list - */ - updateLocations(): void { - this.locations = new RObjectContainer(flatten(this.stars.map(star => star.locations))); - } + /** + * Add a single star + */ + addStar(level = 1, name?: string, x = 0, y = 0): Star { + let result = new Star(this, x, y, name || `Star ${this.stars.length + 1}`); + result.level = level; + this.stars.push(result); + return result; + } - /** - * Generates a random universe, with star systems and locations of interest - * - * This will also : - * - create a network of jump links between star systems - * - add random shops - * - define a progressive gradient of enemy levels - */ - generate(starcount = 50): void { - if (starcount < 4) { - starcount = 4; - } + /** + * Update the locations list + */ + updateLocations(): void { + this.locations = new RObjectContainer(flatten(this.stars.map(star => star.locations))); + } - // Links between stars - while (this.stars.length == 0 || any(this.stars, star => star.getLinks().length == 0)) { - this.stars = this.generateStars(starcount); - - let links = this.getPotentialLinks(); - this.starlinks = this.filterRedundantLinks(this.filterCrossingLinks(links)); - } - this.generateWarpLocations(); - - // Encounter levels - this.setEncounterLevels(); - - // Locations - this.stars.forEach((star: Star) => { - star.generate(this.random); - }); - this.updateLocations(); - this.addShops(); - } - - // Generate a given number of stars, not too crowded - generateStars(count: number): Star[] { - var result: Star[] = []; - - var names = new NameGenerator(Star.NAMES_POOL); - - while (count) { - var x = this.random.random() * this.radius * 2.0 - this.radius; - var y = this.random.random() * this.radius * 2.0 - this.radius; - var star = new Star(this, x, y); - - var nearest = this.getNearestTo(star, result); - if (nearest && nearest.getDistanceTo(star) < this.radius * 0.1) { - continue; - } - - star.name = names.getName() || "Star"; - result.push(star); - - count--; - } - - return result; - } - - // Get a list of potential links between the stars - getPotentialLinks(): StarLink[] { - var result: StarLink[] = []; - - this.stars.forEach((first: Star, idx1: number) => { - this.stars.forEach((second: Star, idx2: number) => { - if (idx1 < idx2) { - if (first.getDistanceTo(second) < this.radius * 0.6) { - result.push(new StarLink(first, second)); - } - } - }); - }); - - return result; - } - - /** - * Filter a list of potential links to avoid crossing ones. - * - * Returned list of links should be free of crossings. - * This should not alter the universe connectivity. - */ - filterCrossingLinks(links: StarLink[]): StarLink[] { - var result: StarLink[] = []; - - links.forEach((link1: StarLink) => { - var crossed = false; - links.forEach((link2: StarLink) => { - if (link1 !== link2 && link1.isCrossing(link2) && link1.getLength() >= link2.getLength()) { - crossed = true; - } - }); - if (!crossed) { - result.push(link1); - } - }); - - return result; - } - - /** - * Filter a list of potential links to remove redundant ones. - * - * This will remove direct links that are also achieved by a similar two-jump couple. - * This should not alter the universe connectivity. - */ - filterRedundantLinks(links: StarLink[]): StarLink[] { - let result: StarLink[] = []; - - links = sortedBy(links, link => link.getLength(), true); - - links.forEach(link => { - let alternative_passages = intersection( - link.first.getNeighbors(links).filter(n => n != link.second), - link.second.getNeighbors(links).filter(n => n != link.first) - ); - let alternative_lengths = alternative_passages.map( - passage => nn(link.first.getLinkTo(passage, links)).getLength() + nn(link.second.getLinkTo(passage, links)).getLength() - ); - if (!any(alternative_lengths, length => length < link.getLength() * 1.2)) { - result.push(link); - } - }); - - return result; - } - - // Generate warp locations for the links between stars - generateWarpLocations() { - this.starlinks.forEach(link => { - let warp1 = link.first.generateWarpLocationTo(link.second, this.random); - let warp2 = link.second.generateWarpLocationTo(link.first, this.random); - - warp1.setJumpDestination(warp2); - warp2.setJumpDestination(warp1); - }); - - } - - // Get the star nearest to another - getNearestTo(star: Star, others = this.stars): Star | null { - if (others.length === 0) { - return null; - } else { - var mindist = this.radius * 2.0; - var nearest: Star | null = null; - others.forEach((istar: Star) => { - if (istar !== star) { - var dist = star.getDistanceTo(istar); - if (dist < mindist) { - nearest = istar; - mindist = dist; - } - } - }); - return nearest; - } - } - - // Check if a link exists between two stars - areLinked(first: Star, second: Star): boolean { - var result = false; - this.starlinks.forEach((link: StarLink) => { - if (link.isLinking(first, second)) { - result = true; - } - }); - return result; - } - - // Add a link between two stars - addLink(first: Star, second: Star): void { - if (!this.areLinked(first, second)) { - this.starlinks.push(new StarLink(first, second)); - } - } - - /** - * Set the average level of encounters in each system - */ - setEncounterLevels(maximal?: number) { - if (!maximal) { - maximal = Math.min(99, Math.ceil(Math.sqrt(this.stars.length))); - } - - // Reset levels - this.stars.forEach(star => star.level = 1); - - // Choose two systems to be the lowest and highest danger zones (not connected directly) - let lowest = this.random.choice(this.stars.filter(star => star.getLinks().length > 1)); - let highest_choices = this.stars.filter(star => star != lowest && !star.getLinkTo(lowest)); - if (highest_choices.length == 0) { - highest_choices = this.stars.filter(star => star != lowest); - } - let highest = this.random.choice(highest_choices); - highest.level = maximal; - - // Make danger gradients - range(this.stars.length).forEach(() => { - this.stars.forEach(star => { - if (star != lowest && star != highest) { - let neighbors = star.getLinks().map(link => nn(link.getPeer(star))); - let minlevel = min(neighbors.map(neighbor => neighbor.level)); - let maxlevel = max(neighbors.map(neighbor => neighbor.level)); - star.level = (minlevel + maxlevel) / 2; - } - }); - }); - - // Round levels - this.stars.forEach(star => star.level = Math.round(star.level)); - } - - /** - * Add random shops - */ - addShops(): void { - this.stars.forEach(star => { - star.locations.forEach(location => { - if (this.random.random() > 0.6) { - location.addShop(star.level); - } - }); - }); - } - - /** - * Get a good start location - */ - getStartLocation(): StarLocation { - let star = minBy(this.stars, star => star.level); - return star.locations[0]; - } - - /** - * Get a location from its ID - */ - getLocation(id: RObjectId | null): StarLocation | null { - return id === null ? null : this.locations.get(id); - } + /** + * Generates a random universe, with star systems and locations of interest + * + * This will also : + * - create a network of jump links between star systems + * - add random shops + * - define a progressive gradient of enemy levels + */ + generate(starcount = 50): void { + if (starcount < 4) { + starcount = 4; } + + // Links between stars + while (this.stars.length == 0 || any(this.stars, star => star.getLinks().length == 0)) { + this.stars = this.generateStars(starcount); + + let links = this.getPotentialLinks(); + this.starlinks = this.filterRedundantLinks(this.filterCrossingLinks(links)); + } + this.generateWarpLocations(); + + // Encounter levels + this.setEncounterLevels(); + + // Locations + this.stars.forEach((star: Star) => { + star.generate(this.random); + }); + this.updateLocations(); + this.addShops(); + } + + // Generate a given number of stars, not too crowded + generateStars(count: number): Star[] { + var result: Star[] = []; + + var names = new NameGenerator(Star.NAMES_POOL); + + while (count) { + var x = this.random.random() * this.radius * 2.0 - this.radius; + var y = this.random.random() * this.radius * 2.0 - this.radius; + var star = new Star(this, x, y); + + var nearest = this.getNearestTo(star, result); + if (nearest && nearest.getDistanceTo(star) < this.radius * 0.1) { + continue; + } + + star.name = names.getName() || "Star"; + result.push(star); + + count--; + } + + return result; + } + + // Get a list of potential links between the stars + getPotentialLinks(): StarLink[] { + var result: StarLink[] = []; + + this.stars.forEach((first: Star, idx1: number) => { + this.stars.forEach((second: Star, idx2: number) => { + if (idx1 < idx2) { + if (first.getDistanceTo(second) < this.radius * 0.6) { + result.push(new StarLink(first, second)); + } + } + }); + }); + + return result; + } + + /** + * Filter a list of potential links to avoid crossing ones. + * + * Returned list of links should be free of crossings. + * This should not alter the universe connectivity. + */ + filterCrossingLinks(links: StarLink[]): StarLink[] { + var result: StarLink[] = []; + + links.forEach((link1: StarLink) => { + var crossed = false; + links.forEach((link2: StarLink) => { + if (link1 !== link2 && link1.isCrossing(link2) && link1.getLength() >= link2.getLength()) { + crossed = true; + } + }); + if (!crossed) { + result.push(link1); + } + }); + + return result; + } + + /** + * Filter a list of potential links to remove redundant ones. + * + * This will remove direct links that are also achieved by a similar two-jump couple. + * This should not alter the universe connectivity. + */ + filterRedundantLinks(links: StarLink[]): StarLink[] { + let result: StarLink[] = []; + + links = sortedBy(links, link => link.getLength(), true); + + links.forEach(link => { + let alternative_passages = intersection( + link.first.getNeighbors(links).filter(n => n != link.second), + link.second.getNeighbors(links).filter(n => n != link.first) + ); + let alternative_lengths = alternative_passages.map( + passage => nn(link.first.getLinkTo(passage, links)).getLength() + nn(link.second.getLinkTo(passage, links)).getLength() + ); + if (!any(alternative_lengths, length => length < link.getLength() * 1.2)) { + result.push(link); + } + }); + + return result; + } + + // Generate warp locations for the links between stars + generateWarpLocations() { + this.starlinks.forEach(link => { + let warp1 = link.first.generateWarpLocationTo(link.second, this.random); + let warp2 = link.second.generateWarpLocationTo(link.first, this.random); + + warp1.setJumpDestination(warp2); + warp2.setJumpDestination(warp1); + }); + + } + + // Get the star nearest to another + getNearestTo(star: Star, others = this.stars): Star | null { + if (others.length === 0) { + return null; + } else { + var mindist = this.radius * 2.0; + var nearest: Star | null = null; + others.forEach((istar: Star) => { + if (istar !== star) { + var dist = star.getDistanceTo(istar); + if (dist < mindist) { + nearest = istar; + mindist = dist; + } + } + }); + return nearest; + } + } + + // Check if a link exists between two stars + areLinked(first: Star, second: Star): boolean { + var result = false; + this.starlinks.forEach((link: StarLink) => { + if (link.isLinking(first, second)) { + result = true; + } + }); + return result; + } + + // Add a link between two stars + addLink(first: Star, second: Star): void { + if (!this.areLinked(first, second)) { + this.starlinks.push(new StarLink(first, second)); + } + } + + /** + * Set the average level of encounters in each system + */ + setEncounterLevels(maximal?: number) { + if (!maximal) { + maximal = Math.min(99, Math.ceil(Math.sqrt(this.stars.length))); + } + + // Reset levels + this.stars.forEach(star => star.level = 1); + + // Choose two systems to be the lowest and highest danger zones (not connected directly) + let lowest = this.random.choice(this.stars.filter(star => star.getLinks().length > 1)); + let highest_choices = this.stars.filter(star => star != lowest && !star.getLinkTo(lowest)); + if (highest_choices.length == 0) { + highest_choices = this.stars.filter(star => star != lowest); + } + let highest = this.random.choice(highest_choices); + highest.level = maximal; + + // Make danger gradients + range(this.stars.length).forEach(() => { + this.stars.forEach(star => { + if (star != lowest && star != highest) { + let neighbors = star.getLinks().map(link => nn(link.getPeer(star))); + let minlevel = min(neighbors.map(neighbor => neighbor.level)); + let maxlevel = max(neighbors.map(neighbor => neighbor.level)); + star.level = (minlevel + maxlevel) / 2; + } + }); + }); + + // Round levels + this.stars.forEach(star => star.level = Math.round(star.level)); + } + + /** + * Add random shops + */ + addShops(): void { + this.stars.forEach(star => { + star.locations.forEach(location => { + if (this.random.random() > 0.6) { + location.addShop(star.level); + } + }); + }); + } + + /** + * Get a good start location + */ + getStartLocation(): StarLocation { + let star = minBy(this.stars, star => star.level); + return star.locations[0]; + } + + /** + * Get a location from its ID + */ + getLocation(id: RObjectId | null): StarLocation | null { + return id === null ? null : this.locations.get(id); + } } diff --git a/src/core/actions/ActionList.spec.ts b/src/core/actions/ActionList.spec.ts index 3482064..1d22ddc 100644 --- a/src/core/actions/ActionList.spec.ts +++ b/src/core/actions/ActionList.spec.ts @@ -1,45 +1,43 @@ -module TK.SpaceTac.Specs { - testing("ActionList", test => { - test.case("lists actions from ship", check => { - let actions = new ActionList(); - check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); +testing("ActionList", test => { + test.case("lists actions from ship", check => { + let actions = new ActionList(); + check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); - let model = new ShipModel(); - let ship = new Ship(null, null, model); - actions.updateFromShip(ship); - check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); + let model = new ShipModel(); + let ship = new Ship(null, null, model); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); - let action1 = new BaseAction("test1"); - let action2 = new BaseAction("test2"); - let mock = check.patch(model, "getActions", () => [action1, action2]); - ship.level.forceLevel(3); - actions.updateFromShip(ship); - check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); - check.called(mock, [[3, []]]); + let action1 = new BaseAction("test1"); + let action2 = new BaseAction("test2"); + let mock = check.patch(model, "getActions", () => [action1, action2]); + ship.level.forceLevel(3); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); + check.called(mock, [[3, []]]); - let up1: ShipUpgrade = { code: "up1" }; - let up2: ShipUpgrade = { code: "up2" }; - check.patch(model, "getLevelUpgrades", () => [up1, up2]); - ship.level.activateUpgrade(up1, true); - actions.updateFromShip(ship); - check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); - check.called(mock, [[3, ["up1"]]]); - }) + let up1: ShipUpgrade = { code: "up1" }; + let up2: ShipUpgrade = { code: "up2" }; + check.patch(model, "getLevelUpgrades", () => [up1, up2]); + ship.level.activateUpgrade(up1, true); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); + check.called(mock, [[3, ["up1"]]]); + }) - test.case("lists toggled actions", check => { - let actions = new ActionList(); - check.equals(actions.listToggled(), [], "init"); + test.case("lists toggled actions", check => { + let actions = new ActionList(); + check.equals(actions.listToggled(), [], "init"); - let action1 = new ToggleAction("test1"); - let action2 = new ToggleAction("test2"); - (actions).from_model = [new BaseAction(), action1, action2]; - check.equals(actions.listToggled(), [], "actions added but not toggled"); + let action1 = new ToggleAction("test1"); + let action2 = new ToggleAction("test2"); + (actions).from_model = [new BaseAction(), action1, action2]; + check.equals(actions.listToggled(), [], "actions added but not toggled"); - actions.toggle(action1, true); - check.equals(actions.listToggled(), [action1], "action1 is toggled"); + actions.toggle(action1, true); + check.equals(actions.listToggled(), [action1], "action1 is toggled"); - actions.toggle(new ToggleAction("test3"), true); - check.equals(actions.listToggled(), [action1], "action3 cannot be toggled"); - }) - }) -} + actions.toggle(new ToggleAction("test3"), true); + check.equals(actions.listToggled(), [action1], "action3 cannot be toggled"); + }) +}) diff --git a/src/core/actions/ActionList.ts b/src/core/actions/ActionList.ts index 52a5879..602e0b0 100644 --- a/src/core/actions/ActionList.ts +++ b/src/core/actions/ActionList.ts @@ -1,132 +1,138 @@ -module TK.SpaceTac { - /** - * List of actions, that may be used by a ship to keep track of available actions - * - * This manages usage count, toggles and cooldown ... - */ - export class ActionList { - // Available actions - private from_model: BaseAction[] = [] - private custom: BaseAction[] = [] +import { RObjectContainer, RObjectId } from "../../common/RObject"; +import { add, copy, first } from "../../common/Tools"; +import { Cooldown } from "../Cooldown"; +import { Ship } from "../Ship"; +import { BaseAction } from "./BaseAction"; +import { EndTurnAction } from "./EndTurnAction"; +import { ToggleAction } from "./ToggleAction"; - // Toggled actions - private toggled = new RObjectContainer() +/** + * List of actions, that may be used by a ship to keep track of available actions + * + * This manages usage count, toggles and cooldown ... + */ +export class ActionList { + // Available actions + private from_model: BaseAction[] = [] + private custom: BaseAction[] = [] - // Active cooldowns - private cooldowns: { [action: number]: Cooldown } = {} + // Toggled actions + private toggled = new RObjectContainer() - /** - * Add a custom action - */ - addCustom(action: T): T { - add(this.custom, action); - return action; - } + // Active cooldowns + private cooldowns: { [action: number]: Cooldown } = {} - /** - * List all actions - */ - listAll(): BaseAction[] { - return this.from_model.concat(this.custom).concat([EndTurnAction.SINGLETON]); - } + /** + * Add a custom action + */ + addCustom(action: T): T { + add(this.custom, action); + return action; + } - /** - * List all currently toggled actions - */ - listToggled(): ToggleAction[] { - let result: ToggleAction[] = []; + /** + * List all actions + */ + listAll(): BaseAction[] { + return this.from_model.concat(this.custom).concat([EndTurnAction.SINGLETON]); + } - this.listAll().forEach(action => { - if (action instanceof ToggleAction && this.isToggled(action)) { - result.push(action); - } - }); + /** + * List all currently toggled actions + */ + listToggled(): ToggleAction[] { + let result: ToggleAction[] = []; - return result; - } + this.listAll().forEach(action => { + if (action instanceof ToggleAction && this.isToggled(action)) { + result.push(action); + } + }); - /** - * List all currently overheated actions - */ - listOverheated(): BaseAction[] { - return this.listAll().filter(action => this.getCooldown(action).heat > 0); - } + return result; + } - /** - * Get an action by its ID - */ - getById(action_id: RObjectId): BaseAction | null { - return first(this.listAll(), action => action.is(action_id)); - } + /** + * List all currently overheated actions + */ + listOverheated(): BaseAction[] { + return this.listAll().filter(action => this.getCooldown(action).heat > 0); + } - /** - * Check if a toggle action is currently active - */ - isToggled(action: ToggleAction): boolean { - return this.toggled.get(action.id) != null; - } + /** + * Get an action by its ID + */ + getById(action_id: RObjectId): BaseAction | null { + return first(this.listAll(), action => action.is(action_id)); + } - /** - * Toggle the status of an action - */ - toggle(action: ToggleAction, active: boolean): boolean { - if (this.getById(action.id)) { - if (active) { - this.toggled.add(action); - } else { - this.toggled.remove(action); - } - } + /** + * Check if a toggle action is currently active + */ + isToggled(action: ToggleAction): boolean { + return this.toggled.get(action.id) != null; + } - return this.toggled.get(action.id) != null; - } - - /** - * Get the cooldown associated with an action - */ - getCooldown(action: BaseAction): Cooldown { - if (this.getById(action.id)) { - if (typeof this.cooldowns[action.id] == "undefined") { - this.cooldowns[action.id] = copy(action.getCooldown()); - } - return this.cooldowns[action.id]; - } else { - console.warn("Action not found, fake cooldown returned", action, this); - return new Cooldown(); - } - } - - /** - * Store an usage count for an action - */ - storeUsage(action: BaseAction, usage = 1): void { - this.getCooldown(action).use(usage); - } - - /** - * Check if an action may be used (in regards to cooldown) - * - * This does not take power into account - */ - isUsable(action: BaseAction): boolean { - if (this.getById(action.id)) { - return this.getCooldown(action).canUse(); - } else { - return false; - } - } - - /** - * Update the actions from a ship. - * - * Beware that this will change the actions IDs. It should typically be done at a battle start. - * This will reset cooldown, toggles and custom actions. - */ - updateFromShip(ship: Ship) { - this.from_model = ship.getModelActions(); - this.toggled = new RObjectContainer(); - this.cooldowns = {}; - this.custom = []; - } + /** + * Toggle the status of an action + */ + toggle(action: ToggleAction, active: boolean): boolean { + if (this.getById(action.id)) { + if (active) { + this.toggled.add(action); + } else { + this.toggled.remove(action); + } } + + return this.toggled.get(action.id) != null; + } + + /** + * Get the cooldown associated with an action + */ + getCooldown(action: BaseAction): Cooldown { + if (this.getById(action.id)) { + if (typeof this.cooldowns[action.id] == "undefined") { + this.cooldowns[action.id] = copy(action.getCooldown()); + } + return this.cooldowns[action.id]; + } else { + console.warn("Action not found, fake cooldown returned", action, this); + return new Cooldown(); + } + } + + /** + * Store an usage count for an action + */ + storeUsage(action: BaseAction, usage = 1): void { + this.getCooldown(action).use(usage); + } + + /** + * Check if an action may be used (in regards to cooldown) + * + * This does not take power into account + */ + isUsable(action: BaseAction): boolean { + if (this.getById(action.id)) { + return this.getCooldown(action).canUse(); + } else { + return false; + } + } + + /** + * Update the actions from a ship. + * + * Beware that this will change the actions IDs. It should typically be done at a battle start. + * This will reset cooldown, toggles and custom actions. + */ + updateFromShip(ship: Ship) { + this.from_model = ship.getModelActions(); + this.toggled = new RObjectContainer(); + this.cooldowns = {}; + this.custom = []; + } } diff --git a/src/core/actions/BaseAction.spec.ts b/src/core/actions/BaseAction.spec.ts index 09f45ba..fea563b 100644 --- a/src/core/actions/BaseAction.spec.ts +++ b/src/core/actions/BaseAction.spec.ts @@ -1,116 +1,114 @@ -module TK.SpaceTac.Specs { - testing("BaseAction", test => { - test.case("may be applied and reverted", check => { - let battle = TestTools.createBattle(); - let ship = nn(battle.playing_ship); - TestTools.setShipModel(ship, 100, 0, 10); - let action = TestTools.addWeapon(ship, 0, 3, 100, 50); - action.configureCooldown(2, 1); +testing("BaseAction", test => { + test.case("may be applied and reverted", check => { + let battle = TestTools.createBattle(); + let ship = nn(battle.playing_ship); + TestTools.setShipModel(ship, 100, 0, 10); + let action = TestTools.addWeapon(ship, 0, 3, 100, 50); + action.configureCooldown(2, 1); - TestTools.actionChain(check, battle, [ - [ship, action, Target.newFromLocation(0, 0)], - [ship, action, Target.newFromLocation(0, 0)], - [ship, EndTurnAction.SINGLETON, undefined], - ], [ - check => { - check.equals(ship.getValue("power"), 10, "power"); - let cooldown = ship.actions.getCooldown(action); - check.equals(cooldown.uses, 0, "uses"); - check.equals(cooldown.heat, 0, "heat"); - }, - check => { - check.equals(ship.getValue("power"), 7, "power"); - let cooldown = ship.actions.getCooldown(action); - check.equals(cooldown.uses, 1, "uses"); - check.equals(cooldown.heat, 0, "heat"); - }, - check => { - check.equals(ship.getValue("power"), 4, "power"); - let cooldown = ship.actions.getCooldown(action); - check.equals(cooldown.uses, 2, "uses"); - check.equals(cooldown.heat, 1, "heat"); - }, - check => { - check.equals(ship.getValue("power"), 10, "power"); - let cooldown = ship.actions.getCooldown(action); - check.equals(cooldown.uses, 0, "uses"); - check.equals(cooldown.heat, 0, "heat"); - }, - ]); - }) + TestTools.actionChain(check, battle, [ + [ship, action, Target.newFromLocation(0, 0)], + [ship, action, Target.newFromLocation(0, 0)], + [ship, EndTurnAction.SINGLETON, undefined], + ], [ + check => { + check.equals(ship.getValue("power"), 10, "power"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 0, "uses"); + check.equals(cooldown.heat, 0, "heat"); + }, + check => { + check.equals(ship.getValue("power"), 7, "power"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 1, "uses"); + check.equals(cooldown.heat, 0, "heat"); + }, + check => { + check.equals(ship.getValue("power"), 4, "power"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 2, "uses"); + check.equals(cooldown.heat, 1, "heat"); + }, + check => { + check.equals(ship.getValue("power"), 10, "power"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 0, "uses"); + check.equals(cooldown.heat, 0, "heat"); + }, + ]); + }) - test.case("checks against remaining power", check => { - let action = new BaseAction("test"); - check.patch(action, "getPowerUsage", () => 3); + test.case("checks against remaining power", check => { + let action = new BaseAction("test"); + check.patch(action, "getPowerUsage", () => 3); - let ship = new Ship(); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.NO_SUCH_ACTION); + let ship = new Ship(); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.NO_SUCH_ACTION); - ship.actions.addCustom(action); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.POWER); + ship.actions.addCustom(action); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.POWER); - ship.setValue("power", 5); - check.equals(action.checkCannotBeApplied(ship), null); - check.equals(action.checkCannotBeApplied(ship, 4), null); - check.equals(action.checkCannotBeApplied(ship, 3), null); - check.equals(action.checkCannotBeApplied(ship, 2), ActionUnavailability.POWER); + ship.setValue("power", 5); + check.equals(action.checkCannotBeApplied(ship), null); + check.equals(action.checkCannotBeApplied(ship, 4), null); + check.equals(action.checkCannotBeApplied(ship, 3), null); + check.equals(action.checkCannotBeApplied(ship, 2), ActionUnavailability.POWER); - ship.setValue("power", 3); - check.equals(action.checkCannotBeApplied(ship), null); + ship.setValue("power", 3); + check.equals(action.checkCannotBeApplied(ship), null); - ship.setValue("power", 2); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.POWER); - }) + ship.setValue("power", 2); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.POWER); + }) - test.case("checks against overheat", check => { - let action = new BaseAction("test"); - let ship = new Ship(); - ship.actions.addCustom(action); - let cooldown = ship.actions.getCooldown(action); + test.case("checks against overheat", check => { + let action = new BaseAction("test"); + let ship = new Ship(); + ship.actions.addCustom(action); + let cooldown = ship.actions.getCooldown(action); - check.equals(action.checkCannotBeApplied(ship), null); + check.equals(action.checkCannotBeApplied(ship), null); - cooldown.use(); - check.equals(action.checkCannotBeApplied(ship), null); + cooldown.use(); + check.equals(action.checkCannotBeApplied(ship), null); - cooldown.configure(2, 3); - check.equals(action.checkCannotBeApplied(ship), null); + cooldown.configure(2, 3); + check.equals(action.checkCannotBeApplied(ship), null); - cooldown.use(); - check.equals(action.checkCannotBeApplied(ship), null); + cooldown.use(); + check.equals(action.checkCannotBeApplied(ship), null); - cooldown.use(); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); + cooldown.use(); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); - cooldown.cool(); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); + cooldown.cool(); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); - cooldown.cool(); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); + cooldown.cool(); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.OVERHEATED); - cooldown.cool(); - check.equals(action.checkCannotBeApplied(ship), null); - }) + cooldown.cool(); + check.equals(action.checkCannotBeApplied(ship), null); + }) - test.case("helps applying a targetting filter", check => { - let fleet1 = new Fleet(); - let fleet2 = new Fleet(); - let ship1a = fleet1.addShip(); - let ship1b = fleet1.addShip(); - let ship2a = fleet2.addShip(); - let ship2b = fleet2.addShip(); - let ships = [ship1a, ship1b, ship2a, ship2b]; + test.case("helps applying a targetting filter", check => { + let fleet1 = new Fleet(); + let fleet2 = new Fleet(); + let ship1a = fleet1.addShip(); + let ship1b = fleet1.addShip(); + let ship2a = fleet2.addShip(); + let ship2b = fleet2.addShip(); + let ships = [ship1a, ship1b, ship2a, ship2b]; - check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL), - [ship1a, ship1b, ship2a, ship2b], "ALL"); - check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL_BUT_SELF), - [ship1b, ship2a, ship2b], "ALL_BUT_SELF"); - check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES), - [ship1a, ship1b], "ALLIES"); - check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES_BUT_SELF), - [ship1b], "ALLIES_BUT_SELF"); - check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ENEMIES), - [ship2a, ship2b], "ENEMIES"); - }); - }); -} + check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL), + [ship1a, ship1b, ship2a, ship2b], "ALL"); + check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALL_BUT_SELF), + [ship1b, ship2a, ship2b], "ALL_BUT_SELF"); + check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES), + [ship1a, ship1b], "ALLIES"); + check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ALLIES_BUT_SELF), + [ship1b], "ALLIES_BUT_SELF"); + check.equals(BaseAction.filterTargets(ship1a, ships, ActionTargettingFilter.ENEMIES), + [ship2a, ship2b], "ENEMIES"); + }); +}); diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index 68c17fa..5bb3fce 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -1,334 +1,342 @@ -module TK.SpaceTac { - /** - * Targetting mode for an action. - * - * This is a hint as to what type of target is required for this action. - */ - export enum ActionTargettingMode { - // Apply immediately on the ship owning the action, without confirmation - SELF, - // Apply on the ship owning the action, with a confirmation - SELF_CONFIRM, - // Apply on one selected ship - SHIP, - // Apply on a space area - SPACE, - // Apply on the ship owning the action, but has an effect on surroundings - SURROUNDINGS - } +import { imaterialize } from "../../common/Iterators"; +import { RObject } from "../../common/RObject"; +import { IArenaLocation } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { Cooldown } from "../Cooldown"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipActionUsedDiff } from "../diffs/ShipActionUsedDiff"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; - /** - * Targetting filter for an action. - * - * This will filter ships inside the targetted area, to determine which will receive the action effects. - */ - export enum ActionTargettingFilter { - // Apply on all ships - ALL, - // Apply on all ships except the actor - ALL_BUT_SELF, - // Apply on all allies, including the actor - ALLIES, - // Apply on all allies, except the actor - ALLIES_BUT_SELF, - // Apply on all enemies - ENEMIES - } - - /** - * Reasons for action unavailibility - */ - export enum ActionUnavailability { - // Ship is not playing - NOT_PLAYING = "Not this ship turn", - // Action is not available - NO_SUCH_ACTION = "Action not available", - // Not enough power remaining - POWER = "Not enough power", - // Action is overheated - OVERHEATED = "Overheated", - // Vigilance is activated - VIGILANCE = "In vigilance", - // Ship is pinned - PINNED = "Pinned", - } - - /** - * Base class for a battle action. - * - * An action should be the only way to modify a battle state. - */ - export class BaseAction extends RObject { - // Identifier code for the type of action - readonly code: string - - // Full name of the action - readonly name: string - - // Cooldown configuration - private cooldown = new Cooldown() - - // Create the action - constructor(name = "Nothing", code?: string) { - super(); - - this.code = code ? code : name.toLowerCase().replace(" ", ""); - this.name = name; - } - - /** - * Get the verb for this action - */ - getVerb(ship: Ship): string { - return "Do"; - } - - /** - * Get the full title for this action (verb and name) - */ - getTitle(ship: Ship): string { - return `${this.getVerb(ship)} ${this.name}`; - } - - /** - * Get the targetting mode - */ - getTargettingMode(ship: Ship): ActionTargettingMode { - return ActionTargettingMode.SELF; - } - - /** - * Get a default target for this action - */ - getDefaultTarget(ship: Ship): Target { - return Target.newFromShip(ship); - } - - /** - * Configure the cooldown for this action - */ - configureCooldown(overheat: number, cooling: number): void { - this.cooldown.configure(overheat, cooling); - } - - /** - * Get the cooldown configuration - */ - getCooldown(): Cooldown { - // TODO Split configuration (readonly) and usage - return this.cooldown; - } - - /** - * Check basic conditions to know if the ship can use this action at all - * - * Method to extend to set conditions - * - * Returns an unavalability reason, null otherwise - */ - checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { - let battle = ship.getBattle(); - if (battle && battle.playing_ship !== ship) { - // Ship is not playing - return ActionUnavailability.NOT_PLAYING; - } - - if (!ship.actions.getById(this.id)) { - return ActionUnavailability.NO_SUCH_ACTION; - } - - // Check AP usage - if (remaining_ap === null) { - remaining_ap = ship.getValue("power"); - } - var ap_usage = this.getPowerUsage(ship, null); - if (remaining_ap < ap_usage) { - return ActionUnavailability.POWER; - } - - // Check cooldown - if (!ship.actions.isUsable(this)) { - return ActionUnavailability.OVERHEATED; - } - - return null; - } - - /** - * Get the power usage, for applying this action on an hypothetical target - * - * If target is null, an estimated cost is returned. - */ - getPowerUsage(ship: Ship, target: Target | null): number { - return 0; - } - - /** - * Get the range of this action, for targetting purpose - */ - getRangeRadius(ship: Ship): number { - return 0; - } - - /** - * Filter a list of ships to return only those impacted by this action - * - * This may be used as an indicator for helping the player in targetting, or to effectively apply the effects - */ - filterImpactedShips(ship: Ship, source: IArenaLocation, target: Target, ships: Ship[]): Ship[] { - return []; - } - - /** - * Get a list of ships impacted by this action - */ - getImpactedShips(ship: Ship, target: Target, source: IArenaLocation = ship.location): Ship[] { - let battle = ship.getBattle(); - if (battle) { - return this.filterImpactedShips(ship, source, target, imaterialize(battle.iships(true))); - } else { - return []; - } - } - - /** - * Helper to apply a targetting filter on a list of ships, to determine which ones are impacted - */ - static filterTargets(source: Ship, ships: Ship[], filter: ActionTargettingFilter): Ship[] { - return ships.filter(ship => { - if (filter == ActionTargettingFilter.ALL) { - return true; - } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) { - return !ship.is(source); - } else if (filter == ActionTargettingFilter.ALLIES) { - return ship.fleet.player.is(source.fleet.player); - } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) { - return ship.fleet.player.is(source.fleet.player) && !ship.is(source); - } else if (filter == ActionTargettingFilter.ENEMIES) { - return !ship.fleet.player.is(source.fleet.player); - } else { - return false; - } - }); - } - - /** - * Get a name to represent the group of ships specified by a target filter - */ - static getFilterDesc(filter: ActionTargettingFilter, plural = true): string { - if (filter == ActionTargettingFilter.ALL) { - return plural ? "ships" : "ship"; - } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) { - return plural ? "other ships" : "other ship"; - } else if (filter == ActionTargettingFilter.ALLIES) { - return plural ? "team members" : "team member"; - } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) { - return plural ? "teammates" : "teammates"; - } else if (filter == ActionTargettingFilter.ENEMIES) { - return plural ? "enemies" : "enemy"; - } else { - return ""; - } - } - - /** - * Check if a target is suitable for this action - * - * Will call checkLocationTarget or checkShipTarget by default - */ - checkTarget(ship: Ship, target: Target): Target | null { - if (this.checkCannotBeApplied(ship)) { - return null; - } else { - if (target.isShip()) { - return this.checkShipTarget(ship, target); - } else { - return this.checkLocationTarget(ship, target); - } - } - } - - // Method to reimplement to check if a space target is suitable - // Must return null if the target can't be applied, an altered target, or the original target - protected checkLocationTarget(ship: Ship, target: Target): Target | null { - return null; - } - - // Method to reimplement to check if a ship target is suitable - // Must return null if the target can't be applied, an altered target, or the original target - protected checkShipTarget(ship: Ship, target: Target): Target | null { - return null; - } - - /** - * Get the full list of diffs caused by applying this action - * - * This does not perform any check, and assumes the action is doable - */ - getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - // Action usage - result.push(new ShipActionUsedDiff(ship, this, target)); - - // Power usage - let cost = this.getPowerUsage(ship, target); - if (cost) { - result = result.concat(ship.getValueDiffs("power", -cost, true)); - } - - // Action effects - result = result.concat(this.getSpecificDiffs(ship, battle, target)); - - return result; - } - - /** - * Method to reimplement to return the diffs specific to this action - */ - protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - return [] - } - - /** - * Apply the action on a battle state - * - * This will first check that the action can be done, then get the battle diffs and apply them. - */ - apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean { - let reject = this.checkCannotBeApplied(ship); - if (reject) { - console.warn(`Action rejected - ${reject}`, ship, this, target); - return false; - } - - let checked_target = this.checkTarget(ship, target); - if (!checked_target) { - console.warn("Action rejected - invalid target", ship, this, target); - return false; - } - - let cost = this.getPowerUsage(ship, checked_target); - if (ship.getValue("power") < cost) { - console.warn("Action rejected - not enough power", ship, this, checked_target); - return false; - } - - let diffs = this.getDiffs(ship, battle, checked_target); - if (diffs.length) { - battle.applyDiffs(diffs); - return true; - } else { - console.error("Could not apply action, no diff produced"); - return false; - } - } - - /** - * Get textual description of effects - */ - getEffectsDescription(): string { - return ""; - } - } +/** + * Targetting mode for an action. + * + * This is a hint as to what type of target is required for this action. + */ +export enum ActionTargettingMode { + // Apply immediately on the ship owning the action, without confirmation + SELF, + // Apply on the ship owning the action, with a confirmation + SELF_CONFIRM, + // Apply on one selected ship + SHIP, + // Apply on a space area + SPACE, + // Apply on the ship owning the action, but has an effect on surroundings + SURROUNDINGS +} + +/** + * Targetting filter for an action. + * + * This will filter ships inside the targetted area, to determine which will receive the action effects. + */ +export enum ActionTargettingFilter { + // Apply on all ships + ALL, + // Apply on all ships except the actor + ALL_BUT_SELF, + // Apply on all allies, including the actor + ALLIES, + // Apply on all allies, except the actor + ALLIES_BUT_SELF, + // Apply on all enemies + ENEMIES +} + +/** + * Reasons for action unavailibility + */ +export enum ActionUnavailability { + // Ship is not playing + NOT_PLAYING = "Not this ship turn", + // Action is not available + NO_SUCH_ACTION = "Action not available", + // Not enough power remaining + POWER = "Not enough power", + // Action is overheated + OVERHEATED = "Overheated", + // Vigilance is activated + VIGILANCE = "In vigilance", + // Ship is pinned + PINNED = "Pinned", +} + +/** + * Base class for a battle action. + * + * An action should be the only way to modify a battle state. + */ +export class BaseAction extends RObject { + // Identifier code for the type of action + readonly code: string + + // Full name of the action + readonly name: string + + // Cooldown configuration + private cooldown = new Cooldown() + + // Create the action + constructor(name = "Nothing", code?: string) { + super(); + + this.code = code ? code : name.toLowerCase().replace(" ", ""); + this.name = name; + } + + /** + * Get the verb for this action + */ + getVerb(ship: Ship): string { + return "Do"; + } + + /** + * Get the full title for this action (verb and name) + */ + getTitle(ship: Ship): string { + return `${this.getVerb(ship)} ${this.name}`; + } + + /** + * Get the targetting mode + */ + getTargettingMode(ship: Ship): ActionTargettingMode { + return ActionTargettingMode.SELF; + } + + /** + * Get a default target for this action + */ + getDefaultTarget(ship: Ship): Target { + return Target.newFromShip(ship); + } + + /** + * Configure the cooldown for this action + */ + configureCooldown(overheat: number, cooling: number): void { + this.cooldown.configure(overheat, cooling); + } + + /** + * Get the cooldown configuration + */ + getCooldown(): Cooldown { + // TODO Split configuration (readonly) and usage + return this.cooldown; + } + + /** + * Check basic conditions to know if the ship can use this action at all + * + * Method to extend to set conditions + * + * Returns an unavalability reason, null otherwise + */ + checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { + let battle = ship.getBattle(); + if (battle && battle.playing_ship !== ship) { + // Ship is not playing + return ActionUnavailability.NOT_PLAYING; + } + + if (!ship.actions.getById(this.id)) { + return ActionUnavailability.NO_SUCH_ACTION; + } + + // Check AP usage + if (remaining_ap === null) { + remaining_ap = ship.getValue("power"); + } + var ap_usage = this.getPowerUsage(ship, null); + if (remaining_ap < ap_usage) { + return ActionUnavailability.POWER; + } + + // Check cooldown + if (!ship.actions.isUsable(this)) { + return ActionUnavailability.OVERHEATED; + } + + return null; + } + + /** + * Get the power usage, for applying this action on an hypothetical target + * + * If target is null, an estimated cost is returned. + */ + getPowerUsage(ship: Ship, target: Target | null): number { + return 0; + } + + /** + * Get the range of this action, for targetting purpose + */ + getRangeRadius(ship: Ship): number { + return 0; + } + + /** + * Filter a list of ships to return only those impacted by this action + * + * This may be used as an indicator for helping the player in targetting, or to effectively apply the effects + */ + filterImpactedShips(ship: Ship, source: IArenaLocation, target: Target, ships: Ship[]): Ship[] { + return []; + } + + /** + * Get a list of ships impacted by this action + */ + getImpactedShips(ship: Ship, target: Target, source: IArenaLocation = ship.location): Ship[] { + let battle = ship.getBattle(); + if (battle) { + return this.filterImpactedShips(ship, source, target, imaterialize(battle.iships(true))); + } else { + return []; + } + } + + /** + * Helper to apply a targetting filter on a list of ships, to determine which ones are impacted + */ + static filterTargets(source: Ship, ships: Ship[], filter: ActionTargettingFilter): Ship[] { + return ships.filter(ship => { + if (filter == ActionTargettingFilter.ALL) { + return true; + } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) { + return !ship.is(source); + } else if (filter == ActionTargettingFilter.ALLIES) { + return ship.fleet.player.is(source.fleet.player); + } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) { + return ship.fleet.player.is(source.fleet.player) && !ship.is(source); + } else if (filter == ActionTargettingFilter.ENEMIES) { + return !ship.fleet.player.is(source.fleet.player); + } else { + return false; + } + }); + } + + /** + * Get a name to represent the group of ships specified by a target filter + */ + static getFilterDesc(filter: ActionTargettingFilter, plural = true): string { + if (filter == ActionTargettingFilter.ALL) { + return plural ? "ships" : "ship"; + } else if (filter == ActionTargettingFilter.ALL_BUT_SELF) { + return plural ? "other ships" : "other ship"; + } else if (filter == ActionTargettingFilter.ALLIES) { + return plural ? "team members" : "team member"; + } else if (filter == ActionTargettingFilter.ALLIES_BUT_SELF) { + return plural ? "teammates" : "teammates"; + } else if (filter == ActionTargettingFilter.ENEMIES) { + return plural ? "enemies" : "enemy"; + } else { + return ""; + } + } + + /** + * Check if a target is suitable for this action + * + * Will call checkLocationTarget or checkShipTarget by default + */ + checkTarget(ship: Ship, target: Target): Target | null { + if (this.checkCannotBeApplied(ship)) { + return null; + } else { + if (target.isShip()) { + return this.checkShipTarget(ship, target); + } else { + return this.checkLocationTarget(ship, target); + } + } + } + + // Method to reimplement to check if a space target is suitable + // Must return null if the target can't be applied, an altered target, or the original target + protected checkLocationTarget(ship: Ship, target: Target): Target | null { + return null; + } + + // Method to reimplement to check if a ship target is suitable + // Must return null if the target can't be applied, an altered target, or the original target + protected checkShipTarget(ship: Ship, target: Target): Target | null { + return null; + } + + /** + * Get the full list of diffs caused by applying this action + * + * This does not perform any check, and assumes the action is doable + */ + getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + // Action usage + result.push(new ShipActionUsedDiff(ship, this, target)); + + // Power usage + let cost = this.getPowerUsage(ship, target); + if (cost) { + result = result.concat(ship.getValueDiffs("power", -cost, true)); + } + + // Action effects + result = result.concat(this.getSpecificDiffs(ship, battle, target)); + + return result; + } + + /** + * Method to reimplement to return the diffs specific to this action + */ + protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + return [] + } + + /** + * Apply the action on a battle state + * + * This will first check that the action can be done, then get the battle diffs and apply them. + */ + apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean { + let reject = this.checkCannotBeApplied(ship); + if (reject) { + console.warn(`Action rejected - ${reject}`, ship, this, target); + return false; + } + + let checked_target = this.checkTarget(ship, target); + if (!checked_target) { + console.warn("Action rejected - invalid target", ship, this, target); + return false; + } + + let cost = this.getPowerUsage(ship, checked_target); + if (ship.getValue("power") < cost) { + console.warn("Action rejected - not enough power", ship, this, checked_target); + return false; + } + + let diffs = this.getDiffs(ship, battle, checked_target); + if (diffs.length) { + battle.applyDiffs(diffs); + return true; + } else { + console.error("Could not apply action, no diff produced"); + return false; + } + } + + /** + * Get textual description of effects + */ + getEffectsDescription(): string { + return ""; + } } diff --git a/src/core/actions/DeployDroneAction.spec.ts b/src/core/actions/DeployDroneAction.spec.ts index 7937cb5..a9cb3d7 100644 --- a/src/core/actions/DeployDroneAction.spec.ts +++ b/src/core/actions/DeployDroneAction.spec.ts @@ -1,60 +1,58 @@ -module TK.SpaceTac.Specs { - testing("DeployDroneAction", test => { - test.case("stores useful information", check => { - let ship = new Ship(); - let action = new DeployDroneAction("testdrone"); - ship.actions.addCustom(action); +testing("DeployDroneAction", test => { + test.case("stores useful information", check => { + let ship = new Ship(); + let action = new DeployDroneAction("testdrone"); + ship.actions.addCustom(action); - check.equals(action.code, "testdrone"); - check.equals(action.getVerb(ship), "Deploy"); + check.equals(action.code, "testdrone"); + check.equals(action.getVerb(ship), "Deploy"); - ship.actions.toggle(action, true); - check.equals(action.getVerb(ship), "Recall"); - }); + ship.actions.toggle(action, true); + check.equals(action.getVerb(ship), "Recall"); + }); - test.case("allows to deploy in range", check => { - let ship = new Ship(); - ship.setArenaPosition(0, 0); - let action = new DeployDroneAction("testdrone", { power: 0 }, { deploy_distance: 8 }); - ship.actions.addCustom(action); + test.case("allows to deploy in range", check => { + let ship = new Ship(); + ship.setArenaPosition(0, 0); + let action = new DeployDroneAction("testdrone", { power: 0 }, { deploy_distance: 8 }); + ship.actions.addCustom(action); - check.equals(action.checkTarget(ship, new Target(8, 0, null)), new Target(8, 0, null)); - check.equals(action.checkTarget(ship, new Target(12, 0, null)), new Target(8, 0, null)); + check.equals(action.checkTarget(ship, new Target(8, 0, null)), new Target(8, 0, null)); + check.equals(action.checkTarget(ship, new Target(12, 0, null)), new Target(8, 0, null)); - let other = new Ship(); - other.setArenaPosition(8, 0); - check.equals(action.checkTarget(ship, new Target(8, 0, other)), null); - }); + let other = new Ship(); + other.setArenaPosition(8, 0); + check.equals(action.checkTarget(ship, new Target(8, 0, other)), null); + }); - test.case("deploys a new drone", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - ship.setArenaPosition(0, 0); - TestTools.setShipModel(ship, 100, 0, 3); + test.case("deploys a new drone", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; + ship.setArenaPosition(0, 0); + TestTools.setShipModel(ship, 100, 0, 3); - let action = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 8, drone_radius: 4, drone_effects: [new DamageEffect(50)] }); - ship.actions.addCustom(action); + let action = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 8, drone_radius: 4, drone_effects: [new DamageEffect(50)] }); + ship.actions.addCustom(action); - TestTools.actionChain(check, battle, [ - [ship, action, new Target(5, 0)], - ], [ - check => { - check.equals(ship.getValue("power"), 3, "power=3"); - check.equals(battle.drones.count(), 0, "drones=0"); - }, - check => { - check.equals(ship.getValue("power"), 1, "power=1"); - check.equals(battle.drones.count(), 1, "drones=1"); + TestTools.actionChain(check, battle, [ + [ship, action, new Target(5, 0)], + ], [ + check => { + check.equals(ship.getValue("power"), 3, "power=3"); + check.equals(battle.drones.count(), 0, "drones=0"); + }, + check => { + check.equals(ship.getValue("power"), 1, "power=1"); + check.equals(battle.drones.count(), 1, "drones=1"); - let drone = battle.drones.list()[0]; - check.equals(drone.code, "testdrone"); - check.same(drone.owner, ship.id); - check.equals(drone.x, 5); - check.equals(drone.y, 0); - check.equals(drone.radius, 4); - compare_effects(check, drone.effects, [new DamageEffect(50)]); - } - ]) - }); - }); -} + let drone = battle.drones.list()[0]; + check.equals(drone.code, "testdrone"); + check.same(drone.owner, ship.id); + check.equals(drone.x, 5); + check.equals(drone.y, 0); + check.equals(drone.radius, 4); + compare_effects(check, drone.effects, [new DamageEffect(50)]); + } + ]) + }); +}); diff --git a/src/core/actions/DeployDroneAction.ts b/src/core/actions/DeployDroneAction.ts index 15ca893..256853a 100644 --- a/src/core/actions/DeployDroneAction.ts +++ b/src/core/actions/DeployDroneAction.ts @@ -1,108 +1,116 @@ -/// +import { any, copyfields, first } from "../../common/Tools"; +import { arenaDistance, ArenaLocation } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { DroneDeployedDiff, DroneRecalledDiff } from "../diffs/DroneDeployedDiff"; +import { Drone } from "../Drone"; +import { BaseEffect } from "../effects/BaseEffect"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { ActionTargettingMode, BaseAction } from "./BaseAction"; +import { ToggleAction, ToggleActionConfig } from "./ToggleAction"; -module TK.SpaceTac { - /** - * Configuration of a drone deployment action - */ - export interface DeployDroneActionConfig { - // Maximal distance the drone may be deployed - deploy_distance: number +/** + * Configuration of a drone deployment action + */ +export interface DeployDroneActionConfig { + // Maximal distance the drone may be deployed + deploy_distance: number - // Effect radius of the deployed drone - drone_radius: number + // Effect radius of the deployed drone + drone_radius: number - // Effects applied to ships in range of the drone - drone_effects: BaseEffect[] - } - - /** - * Action to deploy a drone in space - * - * This is a toggled action, meaning that deploying a drone requires a permanent power supply from the ship - */ - export class DeployDroneAction extends ToggleAction implements DeployDroneActionConfig { - deploy_distance = 0 - drone_radius = 0 - drone_effects: BaseEffect[] = [] - - constructor(name: string, toggle_config?: Partial, drone_config?: Partial, code?: string) { - super(name, toggle_config, code); - - if (drone_config) { - this.configureDrone(drone_config); - } - } - - /** - * Configure the deployed drone - */ - configureDrone(config: Partial): void { - copyfields(config, this); - } - - getVerb(ship: Ship): string { - return ship.actions.isToggled(this) ? "Recall" : "Deploy"; - } - - getTargettingMode(ship: Ship): ActionTargettingMode { - return ship.actions.isToggled(this) ? ActionTargettingMode.SELF : ActionTargettingMode.SPACE; - } - - getDefaultTarget(ship: Ship): Target { - let harmful = any(this.effects, effect => !effect.isBeneficial()); - let distance = this.drone_radius * (harmful ? 1.1 : 0.9); - return Target.newFromLocation( - ship.arena_x + Math.cos(ship.arena_angle) * distance, - ship.arena_y + Math.sin(ship.arena_angle) * distance - ); - } - - getRangeRadius(ship: Ship): number { - return ship.actions.isToggled(this) ? 0 : this.deploy_distance; - } - - filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { - let result = ships.filter(iship => arenaDistance(iship.location, target) <= this.radius); - result = BaseAction.filterTargets(ship, result, this.filter); - return result; - } - - checkLocationTarget(ship: Ship, target: Target): Target { - target = target.constraintInRange(ship.arena_x, ship.arena_y, this.deploy_distance); - return target; - } - - getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - let result = super.getSpecificDiffs(ship, battle, target); - - if (ship.actions.isToggled(this)) { - let drone = first(battle.drones.list(), idrone => this.is(idrone.parent)); - if (drone) { - result.push(new DroneRecalledDiff(drone)); - } else { - return []; - } - } else { - let drone = new Drone(ship, this.code); - drone.parent = this; - drone.x = target.x; - drone.y = target.y; - drone.radius = this.drone_radius; - drone.effects = this.drone_effects; - - result.push(new DroneDeployedDiff(drone)); - } - - return result; - } - - getEffectsDescription(): string { - let desc = `Deploy drone (power usage ${this.power}, max range ${this.deploy_distance}km)`; - let suffix = `on ${BaseAction.getFilterDesc(this.filter)} in ${this.drone_radius}km radius`; - let effects = this.drone_effects.map(effect => { - return "• " + effect.getDescription() + " " + suffix; - }); - return `${desc}:\n${effects.join("\n")}`; - } - } + // Effects applied to ships in range of the drone + drone_effects: BaseEffect[] +} + +/** + * Action to deploy a drone in space + * + * This is a toggled action, meaning that deploying a drone requires a permanent power supply from the ship + */ +export class DeployDroneAction extends ToggleAction implements DeployDroneActionConfig { + deploy_distance = 0 + drone_radius = 0 + drone_effects: BaseEffect[] = [] + + constructor(name: string, toggle_config?: Partial, drone_config?: Partial, code?: string) { + super(name, toggle_config, code); + + if (drone_config) { + this.configureDrone(drone_config); + } + } + + /** + * Configure the deployed drone + */ + configureDrone(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { + return ship.actions.isToggled(this) ? "Recall" : "Deploy"; + } + + getTargettingMode(ship: Ship): ActionTargettingMode { + return ship.actions.isToggled(this) ? ActionTargettingMode.SELF : ActionTargettingMode.SPACE; + } + + getDefaultTarget(ship: Ship): Target { + let harmful = any(this.effects, effect => !effect.isBeneficial()); + let distance = this.drone_radius * (harmful ? 1.1 : 0.9); + return Target.newFromLocation( + ship.arena_x + Math.cos(ship.arena_angle) * distance, + ship.arena_y + Math.sin(ship.arena_angle) * distance + ); + } + + getRangeRadius(ship: Ship): number { + return ship.actions.isToggled(this) ? 0 : this.deploy_distance; + } + + filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { + let result = ships.filter(iship => arenaDistance(iship.location, target) <= this.radius); + result = BaseAction.filterTargets(ship, result, this.filter); + return result; + } + + checkLocationTarget(ship: Ship, target: Target): Target { + target = target.constraintInRange(ship.arena_x, ship.arena_y, this.deploy_distance); + return target; + } + + getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + let result = super.getSpecificDiffs(ship, battle, target); + + if (ship.actions.isToggled(this)) { + let drone = first(battle.drones.list(), idrone => this.is(idrone.parent)); + if (drone) { + result.push(new DroneRecalledDiff(drone)); + } else { + return []; + } + } else { + let drone = new Drone(ship, this.code); + drone.parent = this; + drone.x = target.x; + drone.y = target.y; + drone.radius = this.drone_radius; + drone.effects = this.drone_effects; + + result.push(new DroneDeployedDiff(drone)); + } + + return result; + } + + getEffectsDescription(): string { + let desc = `Deploy drone (power usage ${this.power}, max range ${this.deploy_distance}km)`; + let suffix = `on ${BaseAction.getFilterDesc(this.filter)} in ${this.drone_radius}km radius`; + let effects = this.drone_effects.map(effect => { + return "• " + effect.getDescription() + " " + suffix; + }); + return `${desc}:\n${effects.join("\n")}`; + } } diff --git a/src/core/actions/EndTurnAction.spec.ts b/src/core/actions/EndTurnAction.spec.ts index de9a3b8..f603d45 100644 --- a/src/core/actions/EndTurnAction.spec.ts +++ b/src/core/actions/EndTurnAction.spec.ts @@ -1,199 +1,197 @@ -module TK.SpaceTac.Specs { - testing("EndTurnAction", test => { - test.case("can't be applied to non-playing ship", check => { - let battle = new Battle(); - battle.fleets[0].addShip(); - battle.fleets[0].addShip(); - battle.throwInitiative(); - battle.setPlayingShip(battle.play_order[0]); +testing("EndTurnAction", test => { + test.case("can't be applied to non-playing ship", check => { + let battle = new Battle(); + battle.fleets[0].addShip(); + battle.fleets[0].addShip(); + battle.throwInitiative(); + battle.setPlayingShip(battle.play_order[0]); - let action = new EndTurnAction(); - check.equals(action.checkCannotBeApplied(battle.play_order[0]), ActionUnavailability.NO_SUCH_ACTION); + let action = new EndTurnAction(); + check.equals(action.checkCannotBeApplied(battle.play_order[0]), ActionUnavailability.NO_SUCH_ACTION); - action = EndTurnAction.SINGLETON; - check.equals(action.checkCannotBeApplied(battle.play_order[0]), null); - check.equals(action.checkCannotBeApplied(battle.play_order[1]), ActionUnavailability.NOT_PLAYING); - }); + action = EndTurnAction.SINGLETON; + check.equals(action.checkCannotBeApplied(battle.play_order[0]), null); + check.equals(action.checkCannotBeApplied(battle.play_order[1]), ActionUnavailability.NOT_PLAYING); + }); - test.case("changes active ship", check => { - let battle = TestTools.createBattle(2, 0); + test.case("changes active ship", check => { + let battle = TestTools.createBattle(2, 0); - TestTools.actionChain(check, battle, [ - [battle.play_order[0], EndTurnAction.SINGLETON, undefined], - ], [ - check => { - check.equals(battle.play_index, 0, "play_index is 0"); - check.same(battle.playing_ship, battle.play_order[0], "first ship is playing"); - check.equals(battle.play_order[0].playing, true, "first ship is playing"); - check.equals(battle.play_order[1].playing, false, "second ship is not playing"); - }, - check => { - check.equals(battle.play_index, 1, "play_index is 1"); - check.same(battle.playing_ship, battle.play_order[1], "second ship is playing"); - check.equals(battle.play_order[0].playing, false, "first ship is not playing"); - check.equals(battle.play_order[1].playing, true, "second ship is playing"); - } - ]); - }); + TestTools.actionChain(check, battle, [ + [battle.play_order[0], EndTurnAction.SINGLETON, undefined], + ], [ + check => { + check.equals(battle.play_index, 0, "play_index is 0"); + check.same(battle.playing_ship, battle.play_order[0], "first ship is playing"); + check.equals(battle.play_order[0].playing, true, "first ship is playing"); + check.equals(battle.play_order[1].playing, false, "second ship is not playing"); + }, + check => { + check.equals(battle.play_index, 1, "play_index is 1"); + check.same(battle.playing_ship, battle.play_order[1], "second ship is playing"); + check.equals(battle.play_order[0].playing, false, "first ship is not playing"); + check.equals(battle.play_order[1].playing, true, "second ship is playing"); + } + ]); + }); - test.case("generates power for previous ship", check => { - let battle = TestTools.createBattle(1, 1); - let [ship1, ship2] = battle.play_order; - TestTools.setShipModel(ship1, 100, 0, 10); - let toggle = new ToggleAction("toggle", { power: 2 }); - ship1.actions.addCustom(toggle); - ship1.setValue("power", 6); + test.case("generates power for previous ship", check => { + let battle = TestTools.createBattle(1, 1); + let [ship1, ship2] = battle.play_order; + TestTools.setShipModel(ship1, 100, 0, 10); + let toggle = new ToggleAction("toggle", { power: 2 }); + ship1.actions.addCustom(toggle); + ship1.setValue("power", 6); - TestTools.actionChain(check, battle, [ - [ship1, toggle, undefined], - [ship1, toggle, undefined], - [ship1, EndTurnAction.SINGLETON, undefined], - [ship2, EndTurnAction.SINGLETON, undefined], - [ship1, toggle, undefined], - [ship1, EndTurnAction.SINGLETON, undefined], - [ship2, EndTurnAction.SINGLETON, undefined], - [ship1, EndTurnAction.SINGLETON, undefined], - [ship2, EndTurnAction.SINGLETON, undefined], - [ship1, toggle, undefined], - [ship1, EndTurnAction.SINGLETON, undefined], - ], [ - check => { - check.equals(ship1.getValue("power"), 6, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 4, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 6, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 10, "power value"); - check.same(battle.playing_ship, ship2); - }, - check => { - check.equals(ship1.getValue("power"), 10, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 8, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 8, "power value"); - check.same(battle.playing_ship, ship2); - }, - check => { - check.equals(ship1.getValue("power"), 8, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 8, "power value"); - check.same(battle.playing_ship, ship2); - }, - check => { - check.equals(ship1.getValue("power"), 8, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 10, "power value"); - check.same(battle.playing_ship, ship1); - }, - check => { - check.equals(ship1.getValue("power"), 10, "power value"); - check.same(battle.playing_ship, ship2); - }, - ]); - }); + TestTools.actionChain(check, battle, [ + [ship1, toggle, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + ], [ + check => { + check.equals(ship1.getValue("power"), 6, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 4, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 6, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 10, "power value"); + check.same(battle.playing_ship, ship2); + }, + check => { + check.equals(ship1.getValue("power"), 10, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 8, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 8, "power value"); + check.same(battle.playing_ship, ship2); + }, + check => { + check.equals(ship1.getValue("power"), 8, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 8, "power value"); + check.same(battle.playing_ship, ship2); + }, + check => { + check.equals(ship1.getValue("power"), 8, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 10, "power value"); + check.same(battle.playing_ship, ship1); + }, + check => { + check.equals(ship1.getValue("power"), 10, "power value"); + check.same(battle.playing_ship, ship2); + }, + ]); + }); - test.case("cools down equipment for previous ship", check => { - let battle = TestTools.createBattle(1, 0); - let ship = battle.play_order[0]; + test.case("cools down equipment for previous ship", check => { + let battle = TestTools.createBattle(1, 0); + let ship = battle.play_order[0]; - let equ1 = TestTools.addWeapon(ship); - equ1.configureCooldown(1, 3); - let cd1 = ship.actions.getCooldown(equ1); - cd1.use(); + let equ1 = TestTools.addWeapon(ship); + equ1.configureCooldown(1, 3); + let cd1 = ship.actions.getCooldown(equ1); + cd1.use(); - let equ2 = TestTools.addWeapon(ship); - equ2.configureCooldown(1, 2); - let cd2 = ship.actions.getCooldown(equ2); - cd2.use(); + let equ2 = TestTools.addWeapon(ship); + equ2.configureCooldown(1, 2); + let cd2 = ship.actions.getCooldown(equ2); + cd2.use(); - let equ3 = TestTools.addWeapon(ship); - let cd3 = ship.actions.getCooldown(equ3); - cd3.use(); + let equ3 = TestTools.addWeapon(ship); + let cd3 = ship.actions.getCooldown(equ3); + cd3.use(); - TestTools.actionChain(check, battle, [ - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - ], [ - check => { - check.equals(cd1.heat, 3, "equ1 heat"); - check.equals(cd2.heat, 2, "equ2 heat"); - check.equals(cd3.heat, 0, "equ3 heat"); - }, - check => { - check.equals(cd1.heat, 2, "equ1 heat"); - check.equals(cd2.heat, 1, "equ2 heat"); - check.equals(cd3.heat, 0, "equ3 heat"); - }, - check => { - check.equals(cd1.heat, 1, "equ1 heat"); - check.equals(cd2.heat, 0, "equ2 heat"); - check.equals(cd3.heat, 0, "equ3 heat"); - }, - check => { - check.equals(cd1.heat, 0, "equ1 heat"); - check.equals(cd2.heat, 0, "equ2 heat"); - check.equals(cd3.heat, 0, "equ3 heat"); - } - ]); - }); + TestTools.actionChain(check, battle, [ + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + ], [ + check => { + check.equals(cd1.heat, 3, "equ1 heat"); + check.equals(cd2.heat, 2, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); + }, + check => { + check.equals(cd1.heat, 2, "equ1 heat"); + check.equals(cd2.heat, 1, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); + }, + check => { + check.equals(cd1.heat, 1, "equ1 heat"); + check.equals(cd2.heat, 0, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); + }, + check => { + check.equals(cd1.heat, 0, "equ1 heat"); + check.equals(cd2.heat, 0, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); + } + ]); + }); - test.case("fades sticky effects for previous ship", check => { - let battle = TestTools.createBattle(1, 0); - let ship = battle.play_order[0]; + test.case("fades sticky effects for previous ship", check => { + let battle = TestTools.createBattle(1, 0); + let ship = battle.play_order[0]; - let effect1 = new BaseEffect("e1"); - let effect2 = new StickyEffect(new AttributeLimitEffect("evasion", 7), 2); + let effect1 = new BaseEffect("e1"); + let effect2 = new StickyEffect(new AttributeLimitEffect("evasion", 7), 2); - ship.active_effects.add(effect1); - ship.active_effects.add(effect2); - effect2.base.getOnDiffs(ship, ship).forEach(effect => effect.apply(battle)); - check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship, effect1]]); + ship.active_effects.add(effect1); + ship.active_effects.add(effect2); + effect2.base.getOnDiffs(ship, ship).forEach(effect => effect.apply(battle)); + check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship, effect1]]); - TestTools.actionChain(check, battle, [ - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], - ], [ - check => { - check.equals(ship.active_effects.count(), 2, "effect count"); - check.contains(ship.active_effects.ids(), effect2.id, "sticky effect active"); - check.equals((nn(ship.active_effects.get(effect2.id))).duration, 2, "duration sticky effect"); - check.equals(ship.attributes.evasion.getMaximal(), 7, "max evasion"); - }, - check => { - check.equals(ship.active_effects.count(), 2, "effect count"); - check.contains(ship.active_effects.ids(), effect2.id, "sticky effect active"); - check.equals((nn(ship.active_effects.get(effect2.id))).duration, 1, "duration sticky effect"); - check.equals(ship.attributes.evasion.getMaximal(), 7, "max evasion"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.notcontains(ship.active_effects.ids(), effect2.id, "sticky effect removed"); - check.equals(ship.attributes.evasion.getMaximal(), Infinity, "max evasion"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.notcontains(ship.active_effects.ids(), effect2.id, "sticky effect removed"); - check.equals(ship.attributes.evasion.getMaximal(), Infinity, "max evasion"); - } - ]); - }); - }); -} + TestTools.actionChain(check, battle, [ + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], + ], [ + check => { + check.equals(ship.active_effects.count(), 2, "effect count"); + check.contains(ship.active_effects.ids(), effect2.id, "sticky effect active"); + check.equals((nn(ship.active_effects.get(effect2.id))).duration, 2, "duration sticky effect"); + check.equals(ship.attributes.evasion.getMaximal(), 7, "max evasion"); + }, + check => { + check.equals(ship.active_effects.count(), 2, "effect count"); + check.contains(ship.active_effects.ids(), effect2.id, "sticky effect active"); + check.equals((nn(ship.active_effects.get(effect2.id))).duration, 1, "duration sticky effect"); + check.equals(ship.attributes.evasion.getMaximal(), 7, "max evasion"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.notcontains(ship.active_effects.ids(), effect2.id, "sticky effect removed"); + check.equals(ship.attributes.evasion.getMaximal(), Infinity, "max evasion"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.notcontains(ship.active_effects.ids(), effect2.id, "sticky effect removed"); + check.equals(ship.attributes.evasion.getMaximal(), Infinity, "max evasion"); + } + ]); + }); +}); diff --git a/src/core/actions/EndTurnAction.ts b/src/core/actions/EndTurnAction.ts index 6e2d0b0..4535359 100644 --- a/src/core/actions/EndTurnAction.ts +++ b/src/core/actions/EndTurnAction.ts @@ -1,74 +1,80 @@ -/// +import { iforeach } from "../../common/Iterators"; +import { sum } from "../../common/Tools"; +import { Battle } from "../Battle"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipChangeDiff } from "../diffs/ShipChangeDiff"; +import { ShipCooldownDiff } from "../diffs/ShipCooldownDiff"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { ActionTargettingMode, BaseAction } from "./BaseAction"; -module TK.SpaceTac { - /** - * Action to end the ship's turn - * - * This action is always available (through its singleton) - */ - export class EndTurnAction extends BaseAction { - // Singleton that may be used for all ships - static SINGLETON = new EndTurnAction(); +/** + * Action to end the ship's turn + * + * This action is always available (through its singleton) + */ +export class EndTurnAction extends BaseAction { + // Singleton that may be used for all ships + static SINGLETON = new EndTurnAction(); - constructor() { - super("End turn"); + constructor() { + super("End turn"); + } + + getVerb(ship: Ship): string { + return this.name; + } + + getTitle(ship: Ship): string { + return this.name; + } + + getPowerUsage(ship: Ship, target: Target | null): number { + let toggled_cost = sum(ship.getToggleActions(true).map(action => action.power)); + return ship.getValue("power") + toggled_cost - ship.getAttribute("power_capacity"); + } + + getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + if (ship.is(battle.playing_ship)) { + let result: BaseBattleDiff[] = []; + let new_ship = battle.getNextShip(); + + // Cool down actions + ship.actions.listAll().forEach(action => { + if (ship.actions.getCooldown(action).heat > 0) { + result.push(new ShipCooldownDiff(ship, action, 1)); } + }) - getVerb(ship: Ship): string { - return this.name; - } + // "On turn end" effects + iforeach(ship.active_effects.iterator(), effect => { + result = result.concat(effect.getTurnEndDiffs(ship)); + }); - getTitle(ship: Ship): string { - return this.name; - } + // Change the active ship + let cycle_diff = (battle.play_order.indexOf(new_ship) == 0) ? 1 : 0; + result.push(new ShipChangeDiff(ship, new_ship, cycle_diff)); - getPowerUsage(ship: Ship, target: Target | null): number { - let toggled_cost = sum(ship.getToggleActions(true).map(action => action.power)); - return ship.getValue("power") + toggled_cost - ship.getAttribute("power_capacity"); - } + // "On turn start" effects + iforeach(new_ship.active_effects.iterator(), effect => { + result = result.concat(effect.getTurnStartDiffs(ship)); + }); - getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - if (ship.is(battle.playing_ship)) { - let result: BaseBattleDiff[] = []; - let new_ship = battle.getNextShip(); - - // Cool down actions - ship.actions.listAll().forEach(action => { - if (ship.actions.getCooldown(action).heat > 0) { - result.push(new ShipCooldownDiff(ship, action, 1)); - } - }) - - // "On turn end" effects - iforeach(ship.active_effects.iterator(), effect => { - result = result.concat(effect.getTurnEndDiffs(ship)); - }); - - // Change the active ship - let cycle_diff = (battle.play_order.indexOf(new_ship) == 0) ? 1 : 0; - result.push(new ShipChangeDiff(ship, new_ship, cycle_diff)); - - // "On turn start" effects - iforeach(new_ship.active_effects.iterator(), effect => { - result = result.concat(effect.getTurnStartDiffs(ship)); - }); - - return result; - } else { - return []; - } - } - - protected checkShipTarget(ship: Ship, target: Target): Target | null { - return ship.is(target.ship_id) ? target : null; - } - - getTargettingMode(ship: Ship): ActionTargettingMode { - return ship.getValue("power") ? ActionTargettingMode.SELF_CONFIRM : ActionTargettingMode.SELF; - } - - getEffectsDescription(): string { - return "End the current ship's turn.\nWill also generate power and cool down equipments."; - } + return result; + } else { + return []; } + } + + protected checkShipTarget(ship: Ship, target: Target): Target | null { + return ship.is(target.ship_id) ? target : null; + } + + getTargettingMode(ship: Ship): ActionTargettingMode { + return ship.getValue("power") ? ActionTargettingMode.SELF_CONFIRM : ActionTargettingMode.SELF; + } + + getEffectsDescription(): string { + return "End the current ship's turn.\nWill also generate power and cool down equipments."; + } } diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index 216407e..d341c43 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -1,140 +1,138 @@ -module TK.SpaceTac.Specs { - testing("MoveAction", test => { - test.case("checks movement against remaining AP", check => { - var ship = new Ship(); - var battle = new Battle(ship.fleet); - TestTools.setShipPlaying(battle, ship); - ship.setValue("power", 6); - ship.arena_x = 0; - ship.arena_y = 0; - var action = new MoveAction("Engine", { distance_per_power: 10 }); - ship.actions.addCustom(action); +testing("MoveAction", test => { + test.case("checks movement against remaining AP", check => { + var ship = new Ship(); + var battle = new Battle(ship.fleet); + TestTools.setShipPlaying(battle, ship); + ship.setValue("power", 6); + ship.arena_x = 0; + ship.arena_y = 0; + var action = new MoveAction("Engine", { distance_per_power: 10 }); + ship.actions.addCustom(action); - var result = action.checkTarget(ship, Target.newFromLocation(0, 20)); - check.equals(result, Target.newFromLocation(0, 20)); + var result = action.checkTarget(ship, Target.newFromLocation(0, 20)); + check.equals(result, Target.newFromLocation(0, 20)); - result = action.checkTarget(ship, Target.newFromLocation(0, 80)); - check.nears(nn(result).y, 59.9); + result = action.checkTarget(ship, Target.newFromLocation(0, 80)); + check.nears(nn(result).y, 59.9); - ship.setValue("power", 0); - result = action.checkTarget(ship, Target.newFromLocation(0, 80)); - check.equals(result, null); - }); + ship.setValue("power", 0); + result = action.checkTarget(ship, Target.newFromLocation(0, 80)); + check.equals(result, null); + }); - test.case("forbids targetting a ship", check => { - var ship1 = new Ship(null, "Test1"); - var ship2 = new Ship(null, "Test2"); - var action = new MoveAction(); - ship1.actions.addCustom(action); + test.case("forbids targetting a ship", check => { + var ship1 = new Ship(null, "Test1"); + var ship2 = new Ship(null, "Test2"); + var action = new MoveAction(); + ship1.actions.addCustom(action); - var result = action.checkTarget(ship1, Target.newFromShip(ship1)); - check.equals(result, null); + var result = action.checkTarget(ship1, Target.newFromShip(ship1)); + check.equals(result, null); - result = action.checkTarget(ship1, Target.newFromShip(ship2)); - check.equals(result, null); - }); + result = action.checkTarget(ship1, Target.newFromShip(ship2)); + check.equals(result, null); + }); - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - ship.setArenaPosition(500, 600) - TestTools.setShipModel(ship, 100, 0, 20); - ship.setValue("power", 5); + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; + ship.setArenaPosition(500, 600) + TestTools.setShipModel(ship, 100, 0, 20); + ship.setValue("power", 5); - let action = new MoveAction("Engine", { distance_per_power: 1 }); - ship.actions.addCustom(action); + let action = new MoveAction("Engine", { distance_per_power: 1 }); + ship.actions.addCustom(action); - TestTools.actionChain(check, battle, [ - [ship, action, Target.newFromLocation(510, 605)], - ], [ - check => { - check.equals(ship.arena_x, 500, "ship X"); - check.equals(ship.arena_y, 600, "ship Y"); - check.equals(ship.getValue("power"), 5, "power"); - }, - check => { - check.nears(ship.arena_x, 504.382693, 5, "ship X"); - check.nears(ship.arena_y, 602.191346, 5, "ship Y"); - check.equals(ship.getValue("power"), 0, "power"); - } - ]); - }); + TestTools.actionChain(check, battle, [ + [ship, action, Target.newFromLocation(510, 605)], + ], [ + check => { + check.equals(ship.arena_x, 500, "ship X"); + check.equals(ship.arena_y, 600, "ship Y"); + check.equals(ship.getValue("power"), 5, "power"); + }, + check => { + check.nears(ship.arena_x, 504.382693, 5, "ship X"); + check.nears(ship.arena_y, 602.191346, 5, "ship Y"); + check.equals(ship.getValue("power"), 0, "power"); + } + ]); + }); - test.case("can't move too much near another ship", check => { - var battle = TestTools.createBattle(1, 1); - var ship = battle.fleets[0].ships[0]; - var enemy = battle.fleets[1].ships[0]; - TestTools.setShipModel(ship, 100, 0, 100); - ship.setArenaPosition(500, 500); - enemy.setArenaPosition(1000, 500); + test.case("can't move too much near another ship", check => { + var battle = TestTools.createBattle(1, 1); + var ship = battle.fleets[0].ships[0]; + var enemy = battle.fleets[1].ships[0]; + TestTools.setShipModel(ship, 100, 0, 100); + ship.setArenaPosition(500, 500); + enemy.setArenaPosition(1000, 500); - var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 }); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 }); - var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); - check.equals(result, Target.newFromLocation(700, 500)); + var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); + check.equals(result, Target.newFromLocation(700, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500)); - check.equals(result, Target.newFromLocation(800, 500)); + result = action.checkLocationTarget(ship, Target.newFromLocation(800, 500)); + check.equals(result, Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500)); - check.equals(result, Target.newFromLocation(800, 500)); + result = action.checkLocationTarget(ship, Target.newFromLocation(900, 500)); + check.equals(result, Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500)); - check.equals(result, Target.newFromLocation(800, 500)); + result = action.checkLocationTarget(ship, Target.newFromLocation(1000, 500)); + check.equals(result, Target.newFromLocation(800, 500)); - result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500)); - check.equals(result, Target.newFromLocation(1200, 500)); - }); + result = action.checkLocationTarget(ship, Target.newFromLocation(1200, 500)); + check.equals(result, Target.newFromLocation(1200, 500)); + }); - test.case("exclusion radius is applied correctly over two ships", check => { - var battle = TestTools.createBattle(1, 2); - var ship = battle.fleets[0].ships[0]; - var enemy1 = battle.fleets[1].ships[0]; - var enemy2 = battle.fleets[1].ships[1]; - TestTools.setShipModel(ship, 100, 0, 100); - enemy1.setArenaPosition(0, 800); - enemy2.setArenaPosition(0, 1000); + test.case("exclusion radius is applied correctly over two ships", check => { + var battle = TestTools.createBattle(1, 2); + var ship = battle.fleets[0].ships[0]; + var enemy1 = battle.fleets[1].ships[0]; + var enemy2 = battle.fleets[1].ships[1]; + TestTools.setShipModel(ship, 100, 0, 100); + enemy1.setArenaPosition(0, 800); + enemy2.setArenaPosition(0, 1000); - var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 150 }); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 150 }); - var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1100)); - check.equals(result, Target.newFromLocation(0, 650)); - }); + var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1100)); + check.equals(result, Target.newFromLocation(0, 650)); + }); - test.case("exclusion radius does not make the ship go back", check => { - var battle = TestTools.createBattle(1, 2); - var ship = battle.fleets[0].ships[0]; - var enemy1 = battle.fleets[1].ships[0]; - var enemy2 = battle.fleets[1].ships[1]; - TestTools.setShipModel(ship, 100, 0, 100); - enemy1.setArenaPosition(0, 500); - enemy2.setArenaPosition(0, 800); + test.case("exclusion radius does not make the ship go back", check => { + var battle = TestTools.createBattle(1, 2); + var ship = battle.fleets[0].ships[0]; + var enemy1 = battle.fleets[1].ships[0]; + var enemy2 = battle.fleets[1].ships[1]; + TestTools.setShipModel(ship, 100, 0, 100); + enemy1.setArenaPosition(0, 500); + enemy2.setArenaPosition(0, 800); - var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 600 }); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 600 }); - let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1000)); - check.equals(result, null); - result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1400)); - check.equals(result, Target.newFromLocation(0, 1400)); - }); + let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1000)); + check.equals(result, null); + result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1400)); + check.equals(result, Target.newFromLocation(0, 1400)); + }); - test.case("builds a textual description", check => { - let action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 0 }); - check.equals(action.getEffectsDescription(), "Move: 58km per power point"); + test.case("builds a textual description", check => { + let action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 0 }); + check.equals(action.getEffectsDescription(), "Move: 58km per power point"); - action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 12 }); - check.equals(action.getEffectsDescription(), "Move: 58km per power point (safety: 12km)"); - }); + action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 12 }); + check.equals(action.getEffectsDescription(), "Move: 58km per power point (safety: 12km)"); + }); - test.case("can't be used while in vigilance", check => { - let ship = new Ship(); - TestTools.setShipModel(ship, 10, 10, 10); - let vigilance = ship.actions.addCustom(new VigilanceAction("Vigilance")); - let action = ship.actions.addCustom(new MoveAction("Engine")); + test.case("can't be used while in vigilance", check => { + let ship = new Ship(); + TestTools.setShipModel(ship, 10, 10, 10); + let vigilance = ship.actions.addCustom(new VigilanceAction("Vigilance")); + let action = ship.actions.addCustom(new MoveAction("Engine")); - check.equals(action.checkCannotBeApplied(ship), null); - ship.actions.toggle(vigilance, true); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.VIGILANCE); - }); - }); -} + check.equals(action.checkCannotBeApplied(ship), null); + ship.actions.toggle(vigilance, true); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.VIGILANCE); + }); +}); diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index 5598041..2f56fb3 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -1,149 +1,159 @@ -module TK.SpaceTac { - /** - * Configuration of a trigger action - */ - export interface MoveActionConfig { - // Distance allowed for each power point (raw, without applying maneuvrability) - distance_per_power: number - // Safety distance from other ships - safety_distance: number - } +import { any, copyfields } from "../../common/Tools"; +import { arenaAngle, arenaDistance, ArenaLocation, ArenaLocationAngle } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipMoveDiff } from "../diffs/ShipMoveDiff"; +import { PinnedEffect } from "../effects/PinnedEffect"; +import { ExclusionAreas } from "../ExclusionAreas"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { ActionTargettingMode, ActionUnavailability, BaseAction } from "./BaseAction"; +import { VigilanceAction } from "./VigilanceAction"; - /** - * Action to move the ship to a specific location - */ - export class MoveAction extends BaseAction implements MoveActionConfig { - distance_per_power = 0 - safety_distance = 120 - maneuvrability_factor = 0 - - constructor(name = "Engine", config?: Partial, code = "move") { - super(name, code); - - if (config) { - this.configureEngine(config); - } - } - - /** - * Configure the engine - */ - configureEngine(config: Partial): void { - copyfields(config, this); - } - - getVerb(ship: Ship): string { - return "Move"; - } - - getTitle(ship: Ship): string { - return `Use ${this.name}`; - } - - getTargettingMode(ship: Ship): ActionTargettingMode { - return ActionTargettingMode.SPACE; - } - - getDefaultTarget(ship: Ship): Target { - return Target.newFromLocation(ship.arena_x + Math.cos(ship.arena_angle) * 100, ship.arena_y + Math.sin(ship.arena_angle) * 100); - } - - checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { - let base = super.checkCannotBeApplied(ship, Infinity); - if (base) { - return base; - } - - // Check AP usage - if (remaining_ap === null) { - remaining_ap = ship.getValue("power"); - } - if (remaining_ap < 0.0001) { - return ActionUnavailability.POWER; - } - - // Check vigilance actions - if (any(ship.getToggleActions(true), action => action instanceof VigilanceAction)) { - return ActionUnavailability.VIGILANCE; - } - - // Check pinned status - if (any(ship.getEffects(), effect => effect instanceof PinnedEffect)) { - return ActionUnavailability.PINNED; - } - - return null; - } - - getPowerUsage(ship: Ship, target: Target | null): number { - if (this.distance_per_power == 0) { - return Infinity; - } else if (target) { - let distance = Target.newFromShip(ship).getDistanceTo(target); - return Math.ceil(distance / this.distance_per_power); - } else { - return 0; - } - } - - getRangeRadius(ship: Ship): number { - return this.getRangeRadiusForPower(ship); - } - - /** - * Get the distance reachable with a given power - */ - getRangeRadiusForPower(ship: Ship, power = ship.getValue("power")): number { - return power * this.distance_per_power; - } - - /** - * Get an exclusion helper for this move action - */ - getExclusionAreas(ship: Ship): ExclusionAreas { - return ExclusionAreas.fromShip(ship, this.safety_distance); - } - - /** - * Apply exclusion areas (neer arena borders, or other ships) - */ - applyExclusion(ship: Ship, target: Target): Target { - let exclusion = this.getExclusionAreas(ship); - - let destination = exclusion.stopBefore(new ArenaLocation(target.x, target.y), ship.location); - target = Target.newFromLocation(destination.x, destination.y); - return target; - } - - /** - * Apply reachable range, with remaining power - */ - applyReachableRange(ship: Ship, target: Target, margin = 0.1): Target { - let max_distance = this.getRangeRadius(ship); - max_distance = Math.max(0, max_distance - margin); - return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance); - } - - checkLocationTarget(ship: Ship, target: Target): Target | null { - target = this.applyReachableRange(ship, target); - target = this.applyExclusion(ship, target); - return target.getDistanceTo(ship.location) > 0 ? target : null; - } - - protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - let angle = (arenaDistance(target, ship.location) < 0.00001) ? ship.arena_angle : arenaAngle(ship.location, target); - let destination = new ArenaLocationAngle(target.x, target.y, angle); - return [new ShipMoveDiff(ship, ship.location, destination, this)]; - } - - getEffectsDescription(): string { - let result = `Move: ${this.distance_per_power}km per power point`; - - if (this.safety_distance) { - result += ` (safety: ${this.safety_distance}km)`; - } - - return result; - } - } +/** + * Configuration of a trigger action + */ +export interface MoveActionConfig { + // Distance allowed for each power point (raw, without applying maneuvrability) + distance_per_power: number + // Safety distance from other ships + safety_distance: number +} + +/** + * Action to move the ship to a specific location + */ +export class MoveAction extends BaseAction implements MoveActionConfig { + distance_per_power = 0 + safety_distance = 120 + maneuvrability_factor = 0 + + constructor(name = "Engine", config?: Partial, code = "move") { + super(name, code); + + if (config) { + this.configureEngine(config); + } + } + + /** + * Configure the engine + */ + configureEngine(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { + return "Move"; + } + + getTitle(ship: Ship): string { + return `Use ${this.name}`; + } + + getTargettingMode(ship: Ship): ActionTargettingMode { + return ActionTargettingMode.SPACE; + } + + getDefaultTarget(ship: Ship): Target { + return Target.newFromLocation(ship.arena_x + Math.cos(ship.arena_angle) * 100, ship.arena_y + Math.sin(ship.arena_angle) * 100); + } + + checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { + let base = super.checkCannotBeApplied(ship, Infinity); + if (base) { + return base; + } + + // Check AP usage + if (remaining_ap === null) { + remaining_ap = ship.getValue("power"); + } + if (remaining_ap < 0.0001) { + return ActionUnavailability.POWER; + } + + // Check vigilance actions + if (any(ship.getToggleActions(true), action => action instanceof VigilanceAction)) { + return ActionUnavailability.VIGILANCE; + } + + // Check pinned status + if (any(ship.getEffects(), effect => effect instanceof PinnedEffect)) { + return ActionUnavailability.PINNED; + } + + return null; + } + + getPowerUsage(ship: Ship, target: Target | null): number { + if (this.distance_per_power == 0) { + return Infinity; + } else if (target) { + let distance = Target.newFromShip(ship).getDistanceTo(target); + return Math.ceil(distance / this.distance_per_power); + } else { + return 0; + } + } + + getRangeRadius(ship: Ship): number { + return this.getRangeRadiusForPower(ship); + } + + /** + * Get the distance reachable with a given power + */ + getRangeRadiusForPower(ship: Ship, power = ship.getValue("power")): number { + return power * this.distance_per_power; + } + + /** + * Get an exclusion helper for this move action + */ + getExclusionAreas(ship: Ship): ExclusionAreas { + return ExclusionAreas.fromShip(ship, this.safety_distance); + } + + /** + * Apply exclusion areas (neer arena borders, or other ships) + */ + applyExclusion(ship: Ship, target: Target): Target { + let exclusion = this.getExclusionAreas(ship); + + let destination = exclusion.stopBefore(new ArenaLocation(target.x, target.y), ship.location); + target = Target.newFromLocation(destination.x, destination.y); + return target; + } + + /** + * Apply reachable range, with remaining power + */ + applyReachableRange(ship: Ship, target: Target, margin = 0.1): Target { + let max_distance = this.getRangeRadius(ship); + max_distance = Math.max(0, max_distance - margin); + return target.constraintInRange(ship.arena_x, ship.arena_y, max_distance); + } + + checkLocationTarget(ship: Ship, target: Target): Target | null { + target = this.applyReachableRange(ship, target); + target = this.applyExclusion(ship, target); + return target.getDistanceTo(ship.location) > 0 ? target : null; + } + + protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + let angle = (arenaDistance(target, ship.location) < 0.00001) ? ship.arena_angle : arenaAngle(ship.location, target); + let destination = new ArenaLocationAngle(target.x, target.y, angle); + return [new ShipMoveDiff(ship, ship.location, destination, this)]; + } + + getEffectsDescription(): string { + let result = `Move: ${this.distance_per_power}km per power point`; + + if (this.safety_distance) { + result += ` (safety: ${this.safety_distance}km)`; + } + + return result; + } } diff --git a/src/core/actions/ToggleAction.spec.ts b/src/core/actions/ToggleAction.spec.ts index 7d65717..4c8ef0f 100644 --- a/src/core/actions/ToggleAction.spec.ts +++ b/src/core/actions/ToggleAction.spec.ts @@ -1,35 +1,33 @@ -module TK.SpaceTac.Specs { - testing("ToggleAction", test => { - test.case("returns correct targetting mode", check => { - let action = new ToggleAction("testtoggle"); - let ship = new Ship(); - ship.actions.addCustom(action); +testing("ToggleAction", test => { + test.case("returns correct targetting mode", check => { + let action = new ToggleAction("testtoggle"); + let ship = new Ship(); + ship.actions.addCustom(action); - check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - ship.actions.toggle(action, true); - check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); + ship.actions.toggle(action, true); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - action = new ToggleAction("testtoggle", { radius: 50 }); - ship.actions.addCustom(action); - check.same(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS); + action = new ToggleAction("testtoggle", { radius: 50 }); + ship.actions.addCustom(action); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS); - ship.actions.toggle(action, true); - check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - }) + ship.actions.toggle(action, true); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); + }) - test.case("collects impacted ships", check => { - let action = new ToggleAction("testtoggle", { radius: 50 }); - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - ship1.setArenaPosition(0, 0); - let ship2 = battle.fleets[0].addShip(); - ship2.setArenaPosition(0, 30); - let ship3 = battle.fleets[0].addShip(); - ship3.setArenaPosition(0, 60); + test.case("collects impacted ships", check => { + let action = new ToggleAction("testtoggle", { radius: 50 }); + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + ship1.setArenaPosition(0, 0); + let ship2 = battle.fleets[0].addShip(); + ship2.setArenaPosition(0, 30); + let ship3 = battle.fleets[0].addShip(); + ship3.setArenaPosition(0, 60); - let result = action.getImpactedShips(ship1, Target.newFromShip(ship1)); - check.equals(result, [ship1, ship2]); - }); - }) -} + let result = action.getImpactedShips(ship1, Target.newFromShip(ship1)); + check.equals(result, [ship1, ship2]); + }); +}) diff --git a/src/core/actions/ToggleAction.ts b/src/core/actions/ToggleAction.ts index 2637536..2163f05 100644 --- a/src/core/actions/ToggleAction.ts +++ b/src/core/actions/ToggleAction.ts @@ -1,115 +1,122 @@ -/// +import { copyfields } from "../../common/Tools"; +import { arenaDistance, ArenaLocation } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipActionToggleDiff } from "../diffs/ShipActionToggleDiff"; +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "../diffs/ShipEffectAddedDiff"; +import { BaseEffect } from "../effects/BaseEffect"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { ActionTargettingFilter, ActionTargettingMode, BaseAction } from "./BaseAction"; -module TK.SpaceTac { - /** - * Configuration of a toggle action - */ - export interface ToggleActionConfig { - // Power consumption (while active) - power: number - // Effect radius - radius: number - // Effects applied - effects: BaseEffect[] - // Filtering ships that will receive the effects - filter: ActionTargettingFilter - } - - /** - * Action to toggle some effects on the ship or around it, until next turn start - * - * Toggle actions consume power when activated, and restore it when deactivated - */ - export class ToggleAction extends BaseAction { - power = 1 - radius = 0 - effects: BaseEffect[] = [] - filter = ActionTargettingFilter.ALL - - constructor(name: string, config?: Partial, code?: string) { - super(name, code); - - if (config) { - this.configureToggle(config); - } - } - - /** - * Configure the toggling - */ - configureToggle(config: Partial): void { - copyfields(config, this); - } - - getVerb(ship: Ship): string { - return ship.actions.isToggled(this) ? "Deactivate" : "Activate"; - } - - getTargettingMode(ship: Ship): ActionTargettingMode { - if (ship.actions.isToggled(this) || !this.radius) { - return ActionTargettingMode.SELF_CONFIRM; - } else { - return ActionTargettingMode.SURROUNDINGS; - } - } - - getPowerUsage(ship: Ship, target: Target | null): number { - return ship.actions.isToggled(this) ? -this.power : this.power; - } - - getRangeRadius(ship: Ship): number { - return 0; - } - - filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { - let result = ships.filter(iship => arenaDistance(iship.location, source) <= this.radius); - result = BaseAction.filterTargets(ship, result, this.filter); - return result; - } - - checkShipTarget(ship: Ship, target: Target): Target | null { - return ship.is(target.ship_id) ? target : null; - } - - getSpecificDiffs(ship: Ship, battle: Battle, target: Target, apply_effects = true): BaseBattleDiff[] { - let activated = ship.actions.isToggled(this); - - let result: BaseBattleDiff[] = [ - new ShipActionToggleDiff(ship, this, !activated) - ]; - - let ships = this.getImpactedShips(ship, target, ship.location); - ships.forEach(iship => { - this.effects.forEach(effect => { - if (activated) { - result.push(new ShipEffectRemovedDiff(iship, effect)); - if (apply_effects) { - result = result.concat(effect.getOffDiffs(iship)); - } - } else { - result.push(new ShipEffectAddedDiff(iship, effect)); - if (apply_effects) { - result = result.concat(effect.getOnDiffs(iship, ship)); - } - } - }); - }); - - return result; - } - - getEffectsDescription(): string { - if (this.effects.length == 0) { - return ""; - } - - // TODO filter - let desc = `When active (power usage ${this.power})`; - let effects = this.effects.map(effect => { - let suffix = this.radius ? `on ${BaseAction.getFilterDesc(this.filter)} in ${this.radius}km radius` : "on owner ship"; - return "• " + effect.getDescription() + " " + suffix; - }); - return `${desc}:\n${effects.join("\n")}`; - } - } +/** + * Configuration of a toggle action + */ +export interface ToggleActionConfig { + // Power consumption (while active) + power: number + // Effect radius + radius: number + // Effects applied + effects: BaseEffect[] + // Filtering ships that will receive the effects + filter: ActionTargettingFilter +} + +/** + * Action to toggle some effects on the ship or around it, until next turn start + * + * Toggle actions consume power when activated, and restore it when deactivated + */ +export class ToggleAction extends BaseAction { + power = 1 + radius = 0 + effects: BaseEffect[] = [] + filter = ActionTargettingFilter.ALL + + constructor(name: string, config?: Partial, code?: string) { + super(name, code); + + if (config) { + this.configureToggle(config); + } + } + + /** + * Configure the toggling + */ + configureToggle(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { + return ship.actions.isToggled(this) ? "Deactivate" : "Activate"; + } + + getTargettingMode(ship: Ship): ActionTargettingMode { + if (ship.actions.isToggled(this) || !this.radius) { + return ActionTargettingMode.SELF_CONFIRM; + } else { + return ActionTargettingMode.SURROUNDINGS; + } + } + + getPowerUsage(ship: Ship, target: Target | null): number { + return ship.actions.isToggled(this) ? -this.power : this.power; + } + + getRangeRadius(ship: Ship): number { + return 0; + } + + filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { + let result = ships.filter(iship => arenaDistance(iship.location, source) <= this.radius); + result = BaseAction.filterTargets(ship, result, this.filter); + return result; + } + + checkShipTarget(ship: Ship, target: Target): Target | null { + return ship.is(target.ship_id) ? target : null; + } + + getSpecificDiffs(ship: Ship, battle: Battle, target: Target, apply_effects = true): BaseBattleDiff[] { + let activated = ship.actions.isToggled(this); + + let result: BaseBattleDiff[] = [ + new ShipActionToggleDiff(ship, this, !activated) + ]; + + let ships = this.getImpactedShips(ship, target, ship.location); + ships.forEach(iship => { + this.effects.forEach(effect => { + if (activated) { + result.push(new ShipEffectRemovedDiff(iship, effect)); + if (apply_effects) { + result = result.concat(effect.getOffDiffs(iship)); + } + } else { + result.push(new ShipEffectAddedDiff(iship, effect)); + if (apply_effects) { + result = result.concat(effect.getOnDiffs(iship, ship)); + } + } + }); + }); + + return result; + } + + getEffectsDescription(): string { + if (this.effects.length == 0) { + return ""; + } + + // TODO filter + let desc = `When active (power usage ${this.power})`; + let effects = this.effects.map(effect => { + let suffix = this.radius ? `on ${BaseAction.getFilterDesc(this.filter)} in ${this.radius}km radius` : "on owner ship"; + return "• " + effect.getDescription() + " " + suffix; + }); + return `${desc}:\n${effects.join("\n")}`; + } } diff --git a/src/core/actions/TriggerAction.spec.ts b/src/core/actions/TriggerAction.spec.ts index 05e4a18..e5090ce 100644 --- a/src/core/actions/TriggerAction.spec.ts +++ b/src/core/actions/TriggerAction.spec.ts @@ -1,151 +1,149 @@ -module TK.SpaceTac.Specs { - testing("TriggerAction", test => { - test.case("constructs correctly", check => { - let action = new TriggerAction("testweapon", { power: 4, range: 30, blast: 10 }); - check.equals(action.code, "testweapon"); - check.equals(action.getVerb(), "Fire"); +testing("TriggerAction", test => { + test.case("constructs correctly", check => { + let action = new TriggerAction("testweapon", { power: 4, range: 30, blast: 10 }); + check.equals(action.code, "testweapon"); + check.equals(action.getVerb(), "Fire"); - action = new TriggerAction("testweapon", { blast: 10 }); - check.equals(action.getVerb(), "Trigger"); - }) + action = new TriggerAction("testweapon", { blast: 10 }); + check.equals(action.getVerb(), "Trigger"); + }) - test.case("applies effects to alive ships in blast radius", check => { - let fleet = new Fleet(); - let ship = new Ship(fleet, "ship"); - let effect = new BaseEffect("testeffect"); - let mock_apply = check.patch(effect, "getOnDiffs"); - let action = new TriggerAction("testweapon", { power: 5, range: 100, blast: 10, effects: [effect] }); + test.case("applies effects to alive ships in blast radius", check => { + let fleet = new Fleet(); + let ship = new Ship(fleet, "ship"); + let effect = new BaseEffect("testeffect"); + let mock_apply = check.patch(effect, "getOnDiffs"); + let action = new TriggerAction("testweapon", { power: 5, range: 100, blast: 10, effects: [effect] }); - TestTools.setShipModel(ship, 100, 0, 10); - ship.actions.addCustom(action); + TestTools.setShipModel(ship, 100, 0, 10); + ship.actions.addCustom(action); - let ship1 = new Ship(fleet, "ship1"); - ship1.setArenaPosition(65, 72); - let ship2 = new Ship(fleet, "ship2"); - ship2.setArenaPosition(45, 48); - let ship3 = new Ship(fleet, "ship3"); - ship3.setArenaPosition(45, 48); - ship3.alive = false; + let ship1 = new Ship(fleet, "ship1"); + ship1.setArenaPosition(65, 72); + let ship2 = new Ship(fleet, "ship2"); + ship2.setArenaPosition(45, 48); + let ship3 = new Ship(fleet, "ship3"); + ship3.setArenaPosition(45, 48); + ship3.alive = false; - let battle = new Battle(fleet); - battle.play_order = [ship, ship1, ship2, ship3]; - TestTools.setShipPlaying(battle, ship); - fleet.setBattle(battle); + let battle = new Battle(fleet); + battle.play_order = [ship, ship1, ship2, ship3]; + TestTools.setShipPlaying(battle, ship); + fleet.setBattle(battle); - action.apply(battle, ship, Target.newFromLocation(50, 50)); - check.called(mock_apply, [ - [ship2, ship] - ]); - }) + action.apply(battle, ship, Target.newFromLocation(50, 50)); + check.called(mock_apply, [ + [ship2, ship] + ]); + }) - test.case("transforms ship target in location target, when the weapon has blast radius", check => { - let ship1 = new Ship(); - ship1.setArenaPosition(50, 10); - let ship2 = new Ship(); - ship2.setArenaPosition(150, 10); - let action = TestTools.addWeapon(ship1, 1, 0, 100, 30); + test.case("transforms ship target in location target, when the weapon has blast radius", check => { + let ship1 = new Ship(); + ship1.setArenaPosition(50, 10); + let ship2 = new Ship(); + ship2.setArenaPosition(150, 10); + let action = TestTools.addWeapon(ship1, 1, 0, 100, 30); - let target = action.checkTarget(ship1, new Target(150, 10)); - check.equals(target, new Target(150, 10)); + let target = action.checkTarget(ship1, new Target(150, 10)); + check.equals(target, new Target(150, 10)); - target = action.checkTarget(ship1, Target.newFromShip(ship2)); - check.equals(target, new Target(150, 10)); + target = action.checkTarget(ship1, Target.newFromShip(ship2)); + check.equals(target, new Target(150, 10)); - ship1.setArenaPosition(30, 10); + ship1.setArenaPosition(30, 10); - target = action.checkTarget(ship1, Target.newFromShip(ship2)); - check.equals(target, new Target(130, 10)); + target = action.checkTarget(ship1, Target.newFromShip(ship2)); + check.equals(target, new Target(130, 10)); - ship1.setArenaPosition(0, 10); + ship1.setArenaPosition(0, 10); - target = action.checkTarget(ship1, Target.newFromShip(ship2)); - check.equals(target, new Target(100, 10)); - }) + target = action.checkTarget(ship1, Target.newFromShip(ship2)); + check.equals(target, new Target(100, 10)); + }) - test.case("lists impacted ships", check => { - let ship1 = new Ship(null, "S1"); - ship1.setArenaPosition(10, 50); - let ship2 = new Ship(null, "S2"); - ship2.setArenaPosition(40, 60); - let ship3 = new Ship(null, "S3"); - ship3.setArenaPosition(0, 30); - let ships = [ship1, ship2, ship3]; + test.case("lists impacted ships", check => { + let ship1 = new Ship(null, "S1"); + ship1.setArenaPosition(10, 50); + let ship2 = new Ship(null, "S2"); + ship2.setArenaPosition(40, 60); + let ship3 = new Ship(null, "S3"); + ship3.setArenaPosition(0, 30); + let ships = [ship1, ship2, ship3]; - let action = new TriggerAction("testaction", { range: 50 }); - check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromShip(ship2), ships), [ship2]); - check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(10, 50), ships), []); + let action = new TriggerAction("testaction", { range: 50 }); + check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromShip(ship2), ships), [ship2]); + check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(10, 50), ships), []); - action = new TriggerAction("testaction", { range: 50, blast: 40 }); - check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(20, 20), ships), [ship1, ship3]); + action = new TriggerAction("testaction", { range: 50, blast: 40 }); + check.equals(action.filterImpactedShips(ship1, { x: 0, y: 0 }, Target.newFromLocation(20, 20), ships), [ship1, ship3]); - action = new TriggerAction("testaction", { range: 100, angle: 30 }); - check.equals(action.filterImpactedShips(ship1, { x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]); - }) + action = new TriggerAction("testaction", { range: 100, angle: 30 }); + check.equals(action.filterImpactedShips(ship1, { x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]); + }) - test.case("guesses targetting mode", check => { - let ship = new Ship(); - let action = new TriggerAction("testaction"); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM, "self"); + test.case("guesses targetting mode", check => { + let ship = new Ship(); + let action = new TriggerAction("testaction"); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM, "self"); - action = new TriggerAction("testaction", { range: 50 }); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SHIP, "ship"); + action = new TriggerAction("testaction", { range: 50 }); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SHIP, "ship"); - action = new TriggerAction("testaction", { range: 50, blast: 20 }); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "blast"); + action = new TriggerAction("testaction", { range: 50, blast: 20 }); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "blast"); - action = new TriggerAction("testaction", { blast: 20 }); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS, "surroundings"); + action = new TriggerAction("testaction", { blast: 20 }); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS, "surroundings"); - action = new TriggerAction("testaction", { range: 50, angle: 15 }); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "angle"); - }) + action = new TriggerAction("testaction", { range: 50, angle: 15 }); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "angle"); + }) - test.case("rotates toward the target", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - let action = TestTools.addWeapon(ship, 1, 0, 100, 30); - check.patch(action, "checkTarget", (ship: Ship, target: Target) => target); - check.equals(ship.arena_angle, 0); + test.case("rotates toward the target", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; + let action = TestTools.addWeapon(ship, 1, 0, 100, 30); + check.patch(action, "checkTarget", (ship: Ship, target: Target) => target); + check.equals(ship.arena_angle, 0); - let result = action.apply(battle, ship, Target.newFromLocation(10, 20)); - check.equals(result, true); - check.nears(ship.arena_angle, 1.107, 3); + let result = action.apply(battle, ship, Target.newFromLocation(10, 20)); + check.equals(result, true); + check.nears(ship.arena_angle, 1.107, 3); - result = action.apply(battle, ship, Target.newFromShip(ship)); - check.equals(result, true); - check.nears(ship.arena_angle, 1.107, 3); - }) + result = action.apply(battle, ship, Target.newFromShip(ship)); + check.equals(result, true); + check.nears(ship.arena_angle, 1.107, 3); + }) - test.case("builds a textual description", check => { - let action = new TriggerAction(); - check.equals(action.getEffectsDescription(), ""); + test.case("builds a textual description", check => { + let action = new TriggerAction(); + check.equals(action.getEffectsDescription(), ""); - let effects: BaseEffect[] = [new AttributeMultiplyEffect("evasion", 20)]; - action.configureTrigger({ effects: effects, power: 0 }); - check.equals(action.getEffectsDescription(), "Trigger:\n• evasion +20% on self"); + let effects: BaseEffect[] = [new AttributeMultiplyEffect("evasion", 20)]; + action.configureTrigger({ effects: effects, power: 0 }); + check.equals(action.getEffectsDescription(), "Trigger:\n• evasion +20% on self"); - action.configureTrigger({ effects: effects, power: 2 }); - check.equals(action.getEffectsDescription(), "Trigger (power 2):\n• evasion +20% on self"); + action.configureTrigger({ effects: effects, power: 2 }); + check.equals(action.getEffectsDescription(), "Trigger (power 2):\n• evasion +20% on self"); - action.configureTrigger({ effects: effects, power: 2, range: 120 }); - check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% on target"); + action.configureTrigger({ effects: effects, power: 2, range: 120 }); + check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% on target"); - action.configureTrigger({ effects: effects, power: 2, range: 120, angle: 80 }); - check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% in 80° arc"); + action.configureTrigger({ effects: effects, power: 2, range: 120, angle: 80 }); + check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% in 80° arc"); - action.configureTrigger({ effects: effects, power: 2, range: 120, blast: 100, angle: 80 }); - check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% in 100km radius"); - }) + action.configureTrigger({ effects: effects, power: 2, range: 120, blast: 100, angle: 80 }); + check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• evasion +20% in 100km radius"); + }) - test.case("can't be used while in vigilance", check => { - let ship = new Ship(); - TestTools.setShipModel(ship, 10, 10, 10); - let vigilance = ship.actions.addCustom(new VigilanceAction("Vigilance")); - let action = ship.actions.addCustom(new TriggerAction("Weapon")); + test.case("can't be used while in vigilance", check => { + let ship = new Ship(); + TestTools.setShipModel(ship, 10, 10, 10); + let vigilance = ship.actions.addCustom(new VigilanceAction("Vigilance")); + let action = ship.actions.addCustom(new TriggerAction("Weapon")); - check.equals(action.checkCannotBeApplied(ship), null); - ship.actions.toggle(vigilance, true); - check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.VIGILANCE); - }) - }); -} + check.equals(action.checkCannotBeApplied(ship), null); + ship.actions.toggle(vigilance, true); + check.equals(action.checkCannotBeApplied(ship), ActionUnavailability.VIGILANCE); + }) +}); diff --git a/src/core/actions/TriggerAction.ts b/src/core/actions/TriggerAction.ts index b375518..1d6990e 100644 --- a/src/core/actions/TriggerAction.ts +++ b/src/core/actions/TriggerAction.ts @@ -1,222 +1,232 @@ -/// +import { ifilter, imaterialize } from "../../common/Iterators" +import { any, copyfields, first, minBy } from "../../common/Tools" +import { angularDifference, arenaAngle, arenaDistance, ArenaLocation, ArenaLocationAngle } from "../ArenaLocation" +import { Battle } from "../Battle" +import { BaseBattleDiff } from "../diffs/BaseBattleDiff" +import { ProjectileFiredDiff } from "../diffs/ProjectileFiredDiff" +import { ShipMoveDiff } from "../diffs/ShipMoveDiff" +import { BaseEffect } from "../effects/BaseEffect" +import { Ship } from "../Ship" +import { Target } from "../Target" +import { ActionTargettingMode, ActionUnavailability, BaseAction } from "./BaseAction" +import { MoveAction } from "./MoveAction" +import { VigilanceAction } from "./VigilanceAction" -module TK.SpaceTac { - /** - * Configuration of a trigger action - */ - export interface TriggerActionConfig { - // Effects applied on target - effects: BaseEffect[] - // Power consumption - power: number - // Maximal range of the weapon (distance to target) - range: number - // Radius around the target that will be impacted - blast: number - // Angle of the area between the source and the target that will be impacted - angle: number - } - - /** - * Action to trigger an equipment (for example a weapon), with an optional target - * - * The target will be resolved as a list of ships, on which all the action effects will be applied - */ - export class TriggerAction extends BaseAction implements TriggerActionConfig { - effects: BaseEffect[] = [] - power = 1 - range = 0 - blast = 0 - angle = 0 - - constructor(name?: string, config?: Partial, code?: string) { - super(name, code); - - if (config) { - this.configureTrigger(config); - } - } - - /** - * Configure the triggering and effects of this action - */ - configureTrigger(config: Partial) { - copyfields(config, this); - } - - getVerb(): string { - return this.range ? "Fire" : "Trigger"; - } - - getDefaultTarget(ship: Ship): Target { - if (this.range == 0) { - return Target.newFromShip(ship); - } else { - let battle = ship.getBattle(); - if (battle) { - let harmful = any(this.effects, effect => !effect.isBeneficial()); - let ships = imaterialize(harmful ? battle.ienemies(ship, true) : ifilter(battle.iallies(ship, true), iship => !iship.is(ship))); - let nearest = minBy(ships, iship => arenaDistance(ship.location, iship.location)); - return Target.newFromShip(nearest); - } else { - return Target.newFromShip(ship); - } - } - } - - getTargettingMode(ship: Ship): ActionTargettingMode { - if (this.blast) { - if (this.range) { - return ActionTargettingMode.SPACE; - } else { - return ActionTargettingMode.SURROUNDINGS; - } - } else if (this.range) { - if (this.angle) { - return ActionTargettingMode.SPACE; - } else { - return ActionTargettingMode.SHIP; - } - } else { - return ActionTargettingMode.SELF_CONFIRM; - } - } - - getPowerUsage(ship: Ship, target: Target | null): number { - return this.power; - } - - getRangeRadius(ship: Ship): number { - return this.range; - } - - checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { - let base = super.checkCannotBeApplied(ship, remaining_ap); - if (base) { - return base; - } - - // Check vigilance actions - if (any(ship.getToggleActions(true), action => action instanceof VigilanceAction)) { - // TODO Could allow trigger actions that does not involve a move effect - return ActionUnavailability.VIGILANCE; - } - - return null; - } - - filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { - if (this.blast) { - return ships.filter(ship => arenaDistance(ship.location, target) <= this.blast); - } else if (this.angle) { - let angle = arenaAngle(source, target); - let maxangle = (this.angle * 0.5) * Math.PI / 180; - return ships.filter(iship => { - let dist = arenaDistance(source, iship.location); - if (dist < 0.000001 || dist > this.range) { - return false; - } else { - return Math.abs(angularDifference(arenaAngle(source, iship.location), angle)) < maxangle; - } - }); - } else { - return ships.filter(iship => iship.is(target.ship_id)); - } - } - - checkLocationTarget(ship: Ship, target: Target): Target | null { - if (target && (this.blast > 0 || this.angle > 0)) { - target = target.constraintInRange(ship.arena_x, ship.arena_y, this.range); - return target; - } else { - return null; - } - } - - checkShipTarget(ship: Ship, target: Target): Target | null { - if (this.range > 0 && ship.is(target.ship_id)) { - // No self fire - return null; - } else { - // Check if target is in range - if (this.blast > 0 || this.angle > 0) { - return this.checkLocationTarget(ship, new Target(target.x, target.y)); - } else if (target.isInRange(ship.arena_x, ship.arena_y, this.range)) { - return target; - } else { - return null; - } - } - } - - /** - * Collect the effects applied by this action - */ - getEffects(ship: Ship, target: Target, source = ship.location): [Ship, BaseEffect][] { - let result: [Ship, BaseEffect][] = []; - let ships = this.getImpactedShips(ship, target, source); - ships.forEach(iship => { - this.effects.forEach(effect => result.push([iship, effect])); - }); - return result; - } - - protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - if (arenaDistance(ship.location, target) > 1e-6) { - // Face the target - let angle = arenaAngle(ship.location, target); - if (Math.abs(angularDifference(angle, ship.arena_angle)) > 1e-6) { - let destination = new ArenaLocationAngle(ship.arena_x, ship.arena_y, angle); - let engine = first(ship.actions.listAll(), action => action instanceof MoveAction); - result.push(new ShipMoveDiff(ship, ship.location, destination, engine)); - } - - // Fire a projectile - if (this.range) { - result.push(new ProjectileFiredDiff(ship, this, target)); - } - } - - // Apply effects - let effects = this.getEffects(ship, target); - effects.forEach(([ship_target, effect]) => { - let diffs = effect.getOnDiffs(ship_target, ship); - result = result.concat(diffs); - }); - - return result; - } - - getEffectsDescription(): string { - if (this.effects.length == 0) { - return ""; - } - - let info: string[] = []; - if (this.power) { - info.push(`power ${this.power}`); - } - if (this.range) { - info.push(`range ${this.range}km`); - } - - let desc = (info.length) ? `${this.getVerb()} (${info.join(", ")})` : this.getVerb(); - let effects = this.effects.map(effect => { - let suffix: string; - if (this.blast) { - suffix = `in ${this.blast}km radius`; - } else if (this.angle) { - suffix = `in ${this.angle}° arc`; - } else if (this.range) { - suffix = "on target"; - } else { - suffix = "on self"; - } - return "• " + effect.getDescription() + " " + suffix; - }); - return `${desc}:\n${effects.join("\n")}`; - } - } +/** + * Configuration of a trigger action + */ +export interface TriggerActionConfig { + // Effects applied on target + effects: BaseEffect[] + // Power consumption + power: number + // Maximal range of the weapon (distance to target) + range: number + // Radius around the target that will be impacted + blast: number + // Angle of the area between the source and the target that will be impacted + angle: number +} + +/** + * Action to trigger an equipment (for example a weapon), with an optional target + * + * The target will be resolved as a list of ships, on which all the action effects will be applied + */ +export class TriggerAction extends BaseAction implements TriggerActionConfig { + effects: BaseEffect[] = [] + power = 1 + range = 0 + blast = 0 + angle = 0 + + constructor(name?: string, config?: Partial, code?: string) { + super(name, code); + + if (config) { + this.configureTrigger(config); + } + } + + /** + * Configure the triggering and effects of this action + */ + configureTrigger(config: Partial) { + copyfields(config, this); + } + + getVerb(): string { + return this.range ? "Fire" : "Trigger"; + } + + getDefaultTarget(ship: Ship): Target { + if (this.range == 0) { + return Target.newFromShip(ship); + } else { + let battle = ship.getBattle(); + if (battle) { + let harmful = any(this.effects, effect => !effect.isBeneficial()); + let ships = imaterialize(harmful ? battle.ienemies(ship, true) : ifilter(battle.iallies(ship, true), iship => !iship.is(ship))); + let nearest = minBy(ships, iship => arenaDistance(ship.location, iship.location)); + return Target.newFromShip(nearest); + } else { + return Target.newFromShip(ship); + } + } + } + + getTargettingMode(ship: Ship): ActionTargettingMode { + if (this.blast) { + if (this.range) { + return ActionTargettingMode.SPACE; + } else { + return ActionTargettingMode.SURROUNDINGS; + } + } else if (this.range) { + if (this.angle) { + return ActionTargettingMode.SPACE; + } else { + return ActionTargettingMode.SHIP; + } + } else { + return ActionTargettingMode.SELF_CONFIRM; + } + } + + getPowerUsage(ship: Ship, target: Target | null): number { + return this.power; + } + + getRangeRadius(ship: Ship): number { + return this.range; + } + + checkCannotBeApplied(ship: Ship, remaining_ap: number | null = null): ActionUnavailability | null { + let base = super.checkCannotBeApplied(ship, remaining_ap); + if (base) { + return base; + } + + // Check vigilance actions + if (any(ship.getToggleActions(true), action => action instanceof VigilanceAction)) { + // TODO Could allow trigger actions that does not involve a move effect + return ActionUnavailability.VIGILANCE; + } + + return null; + } + + filterImpactedShips(ship: Ship, source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { + if (this.blast) { + return ships.filter(ship => arenaDistance(ship.location, target) <= this.blast); + } else if (this.angle) { + let angle = arenaAngle(source, target); + let maxangle = (this.angle * 0.5) * Math.PI / 180; + return ships.filter(iship => { + let dist = arenaDistance(source, iship.location); + if (dist < 0.000001 || dist > this.range) { + return false; + } else { + return Math.abs(angularDifference(arenaAngle(source, iship.location), angle)) < maxangle; + } + }); + } else { + return ships.filter(iship => iship.is(target.ship_id)); + } + } + + checkLocationTarget(ship: Ship, target: Target): Target | null { + if (target && (this.blast > 0 || this.angle > 0)) { + target = target.constraintInRange(ship.arena_x, ship.arena_y, this.range); + return target; + } else { + return null; + } + } + + checkShipTarget(ship: Ship, target: Target): Target | null { + if (this.range > 0 && ship.is(target.ship_id)) { + // No self fire + return null; + } else { + // Check if target is in range + if (this.blast > 0 || this.angle > 0) { + return this.checkLocationTarget(ship, new Target(target.x, target.y)); + } else if (target.isInRange(ship.arena_x, ship.arena_y, this.range)) { + return target; + } else { + return null; + } + } + } + + /** + * Collect the effects applied by this action + */ + getEffects(ship: Ship, target: Target, source = ship.location): [Ship, BaseEffect][] { + let result: [Ship, BaseEffect][] = []; + let ships = this.getImpactedShips(ship, target, source); + ships.forEach(iship => { + this.effects.forEach(effect => result.push([iship, effect])); + }); + return result; + } + + protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + if (arenaDistance(ship.location, target) > 1e-6) { + // Face the target + let angle = arenaAngle(ship.location, target); + if (Math.abs(angularDifference(angle, ship.arena_angle)) > 1e-6) { + let destination = new ArenaLocationAngle(ship.arena_x, ship.arena_y, angle); + let engine = first(ship.actions.listAll(), action => action instanceof MoveAction); + result.push(new ShipMoveDiff(ship, ship.location, destination, engine)); + } + + // Fire a projectile + if (this.range) { + result.push(new ProjectileFiredDiff(ship, this, target)); + } + } + + // Apply effects + let effects = this.getEffects(ship, target); + effects.forEach(([ship_target, effect]) => { + let diffs = effect.getOnDiffs(ship_target, ship); + result = result.concat(diffs); + }); + + return result; + } + + getEffectsDescription(): string { + if (this.effects.length == 0) { + return ""; + } + + let info: string[] = []; + if (this.power) { + info.push(`power ${this.power}`); + } + if (this.range) { + info.push(`range ${this.range}km`); + } + + let desc = (info.length) ? `${this.getVerb()} (${info.join(", ")})` : this.getVerb(); + let effects = this.effects.map(effect => { + let suffix: string; + if (this.blast) { + suffix = `in ${this.blast}km radius`; + } else if (this.angle) { + suffix = `in ${this.angle}° arc`; + } else if (this.range) { + suffix = "on target"; + } else { + suffix = "on self"; + } + return "• " + effect.getDescription() + " " + suffix; + }); + return `${desc}:\n${effects.join("\n")}`; + } } diff --git a/src/core/actions/VigilanceAction.spec.ts b/src/core/actions/VigilanceAction.spec.ts index 1577239..b2539a9 100644 --- a/src/core/actions/VigilanceAction.spec.ts +++ b/src/core/actions/VigilanceAction.spec.ts @@ -1,108 +1,106 @@ -module TK.SpaceTac.Specs { - testing("VigilanceAction", test => { - test.case("configures", check => { - let ship = new Ship(); - let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { intruder_count: 3 }, "reactfire"); - ship.actions.addCustom(action); +testing("VigilanceAction", test => { + test.case("configures", check => { + let ship = new Ship(); + let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { intruder_count: 3 }, "reactfire"); + ship.actions.addCustom(action); - check.equals(action.code, "reactfire"); - check.equals(action.getPowerUsage(ship, null), 2); - check.equals(action.radius, 120); - check.equals(action.intruder_count, 3); - check.equals(action.getRangeRadius(ship), 0); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS); - check.equals(action.getVerb(ship), "Watch with"); + check.equals(action.code, "reactfire"); + check.equals(action.getPowerUsage(ship, null), 2); + check.equals(action.radius, 120); + check.equals(action.intruder_count, 3); + check.equals(action.getRangeRadius(ship), 0); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS); + check.equals(action.getVerb(ship), "Watch with"); - ship.actions.toggle(action, true); - check.equals(action.getVerb(ship), "Stop"); - check.equals(action.getPowerUsage(ship, null), -2); - check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - }); + ship.actions.toggle(action, true); + check.equals(action.getVerb(ship), "Stop"); + check.equals(action.getPowerUsage(ship, null), -2); + check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); + }); - test.case("builds a textual description", check => { - let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { - intruder_count: 0, - intruder_effects: [new ValueEffect("hull", -1)] - }); - check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on all incoming ships"); - - action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { - intruder_count: 1, - intruder_effects: [new ValueEffect("hull", -1)] - }); - check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first incoming ship"); - - action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { - intruder_count: 3, - intruder_effects: [new ValueEffect("hull", -1)] - }); - check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first 3 incoming ships"); - - action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120, filter: ActionTargettingFilter.ALLIES }, { - intruder_count: 3, - intruder_effects: [new ValueEffect("hull", -1)] - }); - check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first 3 incoming team members"); - }); - - test.case("handles the vigilance effect to know who to target", check => { - let battle = new Battle(); - let ship1a = battle.fleets[0].addShip(); - ship1a.setArenaPosition(0, 0); - TestTools.setShipModel(ship1a, 10, 0, 5); - let ship1b = battle.fleets[0].addShip(); - ship1b.setArenaPosition(800, 0); - TestTools.setShipModel(ship1b, 10, 0, 5); - let ship2a = battle.fleets[1].addShip(); - ship2a.setArenaPosition(800, 0); - TestTools.setShipModel(ship2a, 10, 0, 5); - let ship2b = battle.fleets[1].addShip(); - ship2b.setArenaPosition(1200, 0); - TestTools.setShipModel(ship2b, 10, 0, 5); - let engine = ship2b.actions.addCustom(new MoveAction("Move", { distance_per_power: 1000, safety_distance: 100 })); - - let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionTargettingFilter.ENEMIES }, { - intruder_effects: [new DamageEffect(1)] - })); - - let diffs = action.getDiffs(ship1a, battle); - check.equals(diffs, [ - new ShipActionUsedDiff(ship1a, action, Target.newFromShip(ship1a)), - new ShipValueDiff(ship1a, "power", -1), - new ShipActionToggleDiff(ship1a, action, true), - new ShipEffectAddedDiff(ship2a, action.effects[0]) - ]); - battle.applyDiffs(diffs); - - check.equals(ship1a.active_effects.list(), []); - check.equals(ship1b.active_effects.list(), []); - check.equals(ship2a.active_effects.list(), [action.effects[0]]); - check.equals(ship2b.active_effects.list(), []); - - check.equals(ship1a.getValue("hull"), 10); - check.equals(ship1b.getValue("hull"), 10); - check.equals(ship2a.getValue("hull"), 10); - check.equals(ship2b.getValue("hull"), 10); - - TestTools.setShipPlaying(battle, ship2b); - battle.applyOneAction(engine.id, Target.newFromLocation(500, 0)); - - check.equals(ship1a.active_effects.list(), []); - check.equals(ship1b.active_effects.list(), []); - check.equals(ship2a.active_effects.list(), [action.effects[0]]); - check.equals(ship2b.active_effects.list(), [action.effects[0]]); - - check.equals(ship1a.getValue("hull"), 10); - check.equals(ship1b.getValue("hull"), 10); - check.equals(ship2a.getValue("hull"), 10); - check.equals(ship2b.getValue("hull"), 9); - - battle.applyOneAction(engine.id, Target.newFromLocation(400, 0)); - check.equals(ship2b.getValue("hull"), 9); - - battle.applyOneAction(engine.id, Target.newFromLocation(1200, 0)); - battle.applyOneAction(engine.id, Target.newFromLocation(700, 0)); - check.equals(ship2b.getValue("hull"), 8); - }); + test.case("builds a textual description", check => { + let action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { + intruder_count: 0, + intruder_effects: [new ValueEffect("hull", -1)] }); -} \ No newline at end of file + check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on all incoming ships"); + + action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { + intruder_count: 1, + intruder_effects: [new ValueEffect("hull", -1)] + }); + check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first incoming ship"); + + action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120 }, { + intruder_count: 3, + intruder_effects: [new ValueEffect("hull", -1)] + }); + check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first 3 incoming ships"); + + action = new VigilanceAction("Reactive Fire", { power: 2, radius: 120, filter: ActionTargettingFilter.ALLIES }, { + intruder_count: 3, + intruder_effects: [new ValueEffect("hull", -1)] + }); + check.equals(action.getEffectsDescription(), "Watch a 120km area (power usage 2):\n• hull -1 on the first 3 incoming team members"); + }); + + test.case("handles the vigilance effect to know who to target", check => { + let battle = new Battle(); + let ship1a = battle.fleets[0].addShip(); + ship1a.setArenaPosition(0, 0); + TestTools.setShipModel(ship1a, 10, 0, 5); + let ship1b = battle.fleets[0].addShip(); + ship1b.setArenaPosition(800, 0); + TestTools.setShipModel(ship1b, 10, 0, 5); + let ship2a = battle.fleets[1].addShip(); + ship2a.setArenaPosition(800, 0); + TestTools.setShipModel(ship2a, 10, 0, 5); + let ship2b = battle.fleets[1].addShip(); + ship2b.setArenaPosition(1200, 0); + TestTools.setShipModel(ship2b, 10, 0, 5); + let engine = ship2b.actions.addCustom(new MoveAction("Move", { distance_per_power: 1000, safety_distance: 100 })); + + let action = ship1a.actions.addCustom(new VigilanceAction("Reactive Shot", { radius: 1000, filter: ActionTargettingFilter.ENEMIES }, { + intruder_effects: [new DamageEffect(1)] + })); + + let diffs = action.getDiffs(ship1a, battle); + check.equals(diffs, [ + new ShipActionUsedDiff(ship1a, action, Target.newFromShip(ship1a)), + new ShipValueDiff(ship1a, "power", -1), + new ShipActionToggleDiff(ship1a, action, true), + new ShipEffectAddedDiff(ship2a, action.effects[0]) + ]); + battle.applyDiffs(diffs); + + check.equals(ship1a.active_effects.list(), []); + check.equals(ship1b.active_effects.list(), []); + check.equals(ship2a.active_effects.list(), [action.effects[0]]); + check.equals(ship2b.active_effects.list(), []); + + check.equals(ship1a.getValue("hull"), 10); + check.equals(ship1b.getValue("hull"), 10); + check.equals(ship2a.getValue("hull"), 10); + check.equals(ship2b.getValue("hull"), 10); + + TestTools.setShipPlaying(battle, ship2b); + battle.applyOneAction(engine.id, Target.newFromLocation(500, 0)); + + check.equals(ship1a.active_effects.list(), []); + check.equals(ship1b.active_effects.list(), []); + check.equals(ship2a.active_effects.list(), [action.effects[0]]); + check.equals(ship2b.active_effects.list(), [action.effects[0]]); + + check.equals(ship1a.getValue("hull"), 10); + check.equals(ship1b.getValue("hull"), 10); + check.equals(ship2a.getValue("hull"), 10); + check.equals(ship2b.getValue("hull"), 9); + + battle.applyOneAction(engine.id, Target.newFromLocation(400, 0)); + check.equals(ship2b.getValue("hull"), 9); + + battle.applyOneAction(engine.id, Target.newFromLocation(1200, 0)); + battle.applyOneAction(engine.id, Target.newFromLocation(700, 0)); + check.equals(ship2b.getValue("hull"), 8); + }); +}); diff --git a/src/core/actions/VigilanceAction.ts b/src/core/actions/VigilanceAction.ts index ffaccd9..fc71607 100644 --- a/src/core/actions/VigilanceAction.ts +++ b/src/core/actions/VigilanceAction.ts @@ -1,66 +1,72 @@ -/// +import { copyfields } from "../../common/Tools"; +import { Battle } from "../Battle"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { BaseEffect } from "../effects/BaseEffect"; +import { VigilanceEffect } from "../effects/VigilanceEffect"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { BaseAction } from "./BaseAction"; +import { ToggleAction, ToggleActionConfig } from "./ToggleAction"; -module TK.SpaceTac { - /** - * Configuration of a vigilance action - */ - export interface VigilanceActionConfig { - // Maximal number of trespassing ships before deactivating (0 for unlimited) - intruder_count: number - // Effects to be applied on ships entering the area - intruder_effects: BaseEffect[] - } - - /** - * Action to watch the ship surroundings, and trigger specific effects on any ship that enters the area - */ - export class VigilanceAction extends ToggleAction implements VigilanceActionConfig { - intruder_count = 1; - intruder_effects: BaseEffect[] = []; - - constructor(name: string, toggle_config?: Partial, vigilance_config?: Partial, code?: string) { - super(name, toggle_config, code); - - if (vigilance_config) { - this.configureVigilance(vigilance_config); - } - } - - /** - * Configure the deployed drone - */ - configureVigilance(config: Partial): void { - copyfields(config, this); - this.effects = [new VigilanceEffect(this)]; - } - - getVerb(ship: Ship): string { - return ship.actions.isToggled(this) ? "Stop" : "Watch with"; - } - - getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { - // Do not apply effects, only register the VigilanceEffect on the ships already in the area - let result = super.getSpecificDiffs(ship, battle, target, false); - return result; - } - - getEffectsDescription(): string { - let desc = `Watch a ${this.radius}km area (power usage ${this.power})`; - - let suffix: string; - if (this.intruder_count == 0) { - suffix = `on all incoming ${BaseAction.getFilterDesc(this.filter)}`; - } else if (this.intruder_count == 1) { - suffix = `on the first incoming ${BaseAction.getFilterDesc(this.filter, false)}`; - } else { - suffix = `on the first ${this.intruder_count} incoming ${BaseAction.getFilterDesc(this.filter)}`; - } - - let effects = this.intruder_effects.map(effect => { - return "• " + effect.getDescription() + " " + suffix; - }); - - return `${desc}:\n${effects.join("\n")}`; - } - } +/** + * Configuration of a vigilance action + */ +export interface VigilanceActionConfig { + // Maximal number of trespassing ships before deactivating (0 for unlimited) + intruder_count: number + // Effects to be applied on ships entering the area + intruder_effects: BaseEffect[] +} + +/** + * Action to watch the ship surroundings, and trigger specific effects on any ship that enters the area + */ +export class VigilanceAction extends ToggleAction implements VigilanceActionConfig { + intruder_count = 1; + intruder_effects: BaseEffect[] = []; + + constructor(name: string, toggle_config?: Partial, vigilance_config?: Partial, code?: string) { + super(name, toggle_config, code); + + if (vigilance_config) { + this.configureVigilance(vigilance_config); + } + } + + /** + * Configure the deployed drone + */ + configureVigilance(config: Partial): void { + copyfields(config, this); + this.effects = [new VigilanceEffect(this)]; + } + + getVerb(ship: Ship): string { + return ship.actions.isToggled(this) ? "Stop" : "Watch with"; + } + + getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + // Do not apply effects, only register the VigilanceEffect on the ships already in the area + let result = super.getSpecificDiffs(ship, battle, target, false); + return result; + } + + getEffectsDescription(): string { + let desc = `Watch a ${this.radius}km area (power usage ${this.power})`; + + let suffix: string; + if (this.intruder_count == 0) { + suffix = `on all incoming ${BaseAction.getFilterDesc(this.filter)}`; + } else if (this.intruder_count == 1) { + suffix = `on the first incoming ${BaseAction.getFilterDesc(this.filter, false)}`; + } else { + suffix = `on the first ${this.intruder_count} incoming ${BaseAction.getFilterDesc(this.filter)}`; + } + + let effects = this.intruder_effects.map(effect => { + return "• " + effect.getDescription() + " " + suffix; + }); + + return `${desc}:\n${effects.join("\n")}`; + } } diff --git a/src/core/ai/AIWorker.ts b/src/core/ai/AIWorker.ts index d818fdb..46263e5 100644 --- a/src/core/ai/AIWorker.ts +++ b/src/core/ai/AIWorker.ts @@ -1,93 +1,100 @@ -module TK.SpaceTac { - /** - * Initialize the background worker, if possible - */ - function initializeWorker(): Worker | null { - if (typeof window != "undefined" && (window).Worker) { - try { - return new Worker('aiworker.js'); // TODO not hard-coded - } catch { - console.error("Could not initialize AI web worker"); - return null; - } - } else { - return null; - } - } - - /** - * AI processing, either in the current process or in a web worker - */ - export class AIWorker { - private battle: Battle; - private ship: Ship; - private debug: boolean; - private static worker = initializeWorker(); - - constructor(battle: Battle, debug = false) { - this.battle = battle; - this.ship = nn(battle.playing_ship); - this.debug = debug; - } - - /** - * Process the current playing ship with an AI - * - * This should be done on the real battle state - */ - static async process(battle: Battle, debug = false): Promise { - let processing = new AIWorker(battle, debug); - await processing.processAuto(maneuver => maneuver.apply(battle)); - } - - /** - * Process AI in a webworker if possible, else do the work in the render thread - */ - async processAuto(feedback: AIFeedback): Promise { - if (!this.debug && AIWorker.worker) { - try { - await this.processInWorker(AIWorker.worker, feedback); - } catch (err) { - console.error("Web worker error, falling back to main thread", err); - await this.processHere(feedback); - } - } else { - await this.processHere(feedback); - } - } - - /** - * Process AI in a webworker - */ - async processInWorker(worker: Worker, feedback: AIFeedback): Promise { - let serializer = new Serializer(TK.SpaceTac); - let promise = new Promise((resolve, reject) => { - worker.onerror = reject; - worker.onmessage = (message) => { - let maneuver = serializer.unserialize(message.data); - if (maneuver instanceof Maneuver) { - if (this.debug) { - console.log("Received from AI worker", maneuver); - } - let result = maneuver.apply(this.battle); - if (!result) { - resolve(); - } - } else { - reject("Received something that is not a Maneuver"); - } - }; - }); - worker.postMessage(serializer.serialize(this.battle)); - await promise; - } - - /** - * Process AI in current thread - */ - async processHere(feedback: AIFeedback): Promise { - let ai = new TacticalAI(this.ship, feedback, this.debug); - await ai.play(); - } +import { NAMESPACE } from "../.."; +import { Serializer } from "../../common/Serializer"; +import { nn } from "../../common/Tools"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { AIFeedback } from "./AbstractAI"; +import { Maneuver } from "./Maneuver"; +import { TacticalAI } from "./TacticalAI"; + +/** + * Initialize the background worker, if possible + */ +function initializeWorker(): Worker | null { + if (typeof window != "undefined" && (window).Worker) { + try { + return new Worker('aiworker.js'); // TODO not hard-coded + } catch { + console.error("Could not initialize AI web worker"); + return null; } + } else { + return null; + } +} + +/** + * AI processing, either in the current process or in a web worker + */ +export class AIWorker { + private battle: Battle; + private ship: Ship; + private debug: boolean; + private static worker = initializeWorker(); + + constructor(battle: Battle, debug = false) { + this.battle = battle; + this.ship = nn(battle.playing_ship); + this.debug = debug; + } + + /** + * Process the current playing ship with an AI + * + * This should be done on the real battle state + */ + static async process(battle: Battle, debug = false): Promise { + let processing = new AIWorker(battle, debug); + await processing.processAuto(maneuver => maneuver.apply(battle)); + } + + /** + * Process AI in a webworker if possible, else do the work in the render thread + */ + async processAuto(feedback: AIFeedback): Promise { + if (!this.debug && AIWorker.worker) { + try { + await this.processInWorker(AIWorker.worker, feedback); + } catch (err) { + console.error("Web worker error, falling back to main thread", err); + await this.processHere(feedback); + } + } else { + await this.processHere(feedback); + } + } + + /** + * Process AI in a webworker + */ + async processInWorker(worker: Worker, feedback: AIFeedback): Promise { + let serializer = new Serializer(NAMESPACE); + let promise = new Promise((resolve, reject) => { + worker.onerror = reject; + worker.onmessage = (message) => { + let maneuver = serializer.unserialize(message.data); + if (maneuver instanceof Maneuver) { + if (this.debug) { + console.log("Received from AI worker", maneuver); + } + let result = maneuver.apply(this.battle); + if (!result) { + resolve(); + } + } else { + reject("Received something that is not a Maneuver"); + } + }; + }); + worker.postMessage(serializer.serialize(this.battle)); + await promise; + } + + /** + * Process AI in current thread + */ + async processHere(feedback: AIFeedback): Promise { + let ai = new TacticalAI(this.ship, feedback, this.debug); + await ai.play(); + } } diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/AbstractAI.ts index 5a3f21b..c8d05a8 100644 --- a/src/core/ai/AbstractAI.ts +++ b/src/core/ai/AbstractAI.ts @@ -1,102 +1,108 @@ -module TK.SpaceTac { - /** - * Feeback that will be called with each proposed maneuver, and should return true if the AI is to continue playing - */ - export type AIFeedback = (maneuver: Maneuver) => boolean; +import { RandomGenerator } from "../../common/RandomGenerator"; +import { Timer } from "../../common/Timer"; +import { classname, nn } from "../../common/Tools"; +import { EndTurnAction } from "../actions/EndTurnAction"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { Maneuver } from "./Maneuver"; - /** - * Base class for all Artificial Intelligence interaction - */ - export class AbstractAI { - // Name of the AI - name: string +/** + * Feeback that will be called with each proposed maneuver, and should return true if the AI is to continue playing + */ +export type AIFeedback = (maneuver: Maneuver) => boolean; - // Current ship being played - ship: Ship +/** + * Base class for all Artificial Intelligence interaction + */ +export class AbstractAI { + // Name of the AI + name: string - // Random generator, if needed - random = RandomGenerator.global + // Current ship being played + ship: Ship - // Timer for scheduled calls - timer: Timer + // Random generator, if needed + random = RandomGenerator.global - // Debug mode - debug = false + // Timer for scheduled calls + timer: Timer - // Feedback to send maneuvers to - feedback: AIFeedback + // Debug mode + debug = false - // Time at which work as started - private started = 0 + // Feedback to send maneuvers to + feedback: AIFeedback - constructor(ship: Ship, feedback?: AIFeedback, debug = false, timer = Timer.global, name?: string) { - this.ship = ship; - this.feedback = feedback ? feedback : ((maneuver: Maneuver) => maneuver.apply(nn(this.ship.getBattle()))); - this.debug = debug; - this.timer = timer; - this.name = name || classname(this); - } + // Time at which work as started + private started = 0 - toString = () => this.name; + constructor(ship: Ship, feedback?: AIFeedback, debug = false, timer = Timer.global, name?: string) { + this.ship = ship; + this.feedback = feedback ? feedback : ((maneuver: Maneuver) => maneuver.apply(nn(this.ship.getBattle()))); + this.debug = debug; + this.timer = timer; + this.name = name || classname(this); + } - /** - * Start playing current ship's turn. - */ - async play(): Promise { - this.started = (new Date()).getTime(); + toString = () => this.name; - if (!this.ship.playing) { - console.error(`${this.name} tries to play a ship out of turn`); - return; - } + /** + * Start playing current ship's turn. + */ + async play(): Promise { + this.started = (new Date()).getTime(); - // Work loop - this.initWork(); - let last = new Date().getTime(); - let ship = this.ship; - while (this.doWorkUnit()) { - if (!this.ship.playing || this.ship != ship) { - console.error(`${this.name} switched to another ship in unit work`); - break; - } - if (this.getDuration() >= 10000) { - console.warn(`${this.name} takes too long to play, forcing turn end`); - break; - } - - let t = new Date().getTime(); - if (t - last > 50) { - await this.timer.sleep(10); - last = t + 10; - } - } - - // End the ship turn - if (this.ship.playing) { - this.feedback(new Maneuver(this.ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); - } - } - - /** - * Prepare the groundwork for future doWorkUnit calls - */ - protected initWork(): void { - } - - /** - * Do a single unit of synchronous work - * - * Returns true if something was done, false if the AI should end the ship turn and stop. - */ - protected doWorkUnit(): boolean { - return false; - } - - /** - * Get the time spent thinking on this turn - */ - protected getDuration() { - return (new Date()).getTime() - this.started; - } + if (!this.ship.playing) { + console.error(`${this.name} tries to play a ship out of turn`); + return; } + + // Work loop + this.initWork(); + let last = new Date().getTime(); + let ship = this.ship; + while (this.doWorkUnit()) { + if (!this.ship.playing || this.ship != ship) { + console.error(`${this.name} switched to another ship in unit work`); + break; + } + if (this.getDuration() >= 10000) { + console.warn(`${this.name} takes too long to play, forcing turn end`); + break; + } + + let t = new Date().getTime(); + if (t - last > 50) { + await this.timer.sleep(10); + last = t + 10; + } + } + + // End the ship turn + if (this.ship.playing) { + this.feedback(new Maneuver(this.ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); + } + } + + /** + * Prepare the groundwork for future doWorkUnit calls + */ + protected initWork(): void { + } + + /** + * Do a single unit of synchronous work + * + * Returns true if something was done, false if the AI should end the ship turn and stop. + */ + protected doWorkUnit(): boolean { + return false; + } + + /** + * Get the time spent thinking on this turn + */ + protected getDuration() { + return (new Date()).getTime() - this.started; + } } diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index 55d2903..270da6a 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -1,35 +1,33 @@ -module TK.SpaceTac.Specs { - testing("Maneuver", test => { - test.case("uses move-fire simulation to build a list of battle diffs", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[1].addShip(); - let ship3 = battle.fleets[1].addShip(); - let ship4 = battle.fleets[1].addShip(); - ship1.setArenaPosition(0, 0); - TestTools.setShipModel(ship1, 20, 20, 10); - ship2.setArenaPosition(500, 0); - TestTools.setShipModel(ship2, 70, 100); - ship3.setArenaPosition(560, 0); - TestTools.setShipModel(ship3, 80, 30); - ship4.setArenaPosition(640, 0); - TestTools.setShipModel(ship4, 30, 30); +testing("Maneuver", test => { + test.case("uses move-fire simulation to build a list of battle diffs", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); + let ship3 = battle.fleets[1].addShip(); + let ship4 = battle.fleets[1].addShip(); + ship1.setArenaPosition(0, 0); + TestTools.setShipModel(ship1, 20, 20, 10); + ship2.setArenaPosition(500, 0); + TestTools.setShipModel(ship2, 70, 100); + ship3.setArenaPosition(560, 0); + TestTools.setShipModel(ship3, 80, 30); + ship4.setArenaPosition(640, 0); + TestTools.setShipModel(ship4, 30, 30); - let weapon = TestTools.addWeapon(ship1, 50, 2, 200, 100); - let engine = TestTools.addEngine(ship1, 100); + let weapon = TestTools.addWeapon(ship1, 50, 2, 200, 100); + let engine = TestTools.addEngine(ship1, 100); - let maneuver = new Maneuver(ship1, weapon, Target.newFromLocation(530, 0)); - check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, engine, Target.newFromLocation(331, 0)), "engine use"); - check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power"); - check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move"); - check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon use"); - check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power"); - check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power"); - check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value"); - check.contains(maneuver.effects, new ShipValueDiff(ship3, "shield", -30), "ship3 shield value"); - check.contains(maneuver.effects, new ShipValueDiff(ship3, "hull", -20), "ship3 hull value"); - check.contains(maneuver.effects, new ShipDamageDiff(ship2, 0, 50, 0, 50), "ship2 damage"); - check.contains(maneuver.effects, new ShipDamageDiff(ship3, 20, 30, 0, 50), "ship3 damage"); - }); - }); -} + let maneuver = new Maneuver(ship1, weapon, Target.newFromLocation(530, 0)); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, engine, Target.newFromLocation(331, 0)), "engine use"); + check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power"); + check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move"); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon use"); + check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power"); + check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power"); + check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value"); + check.contains(maneuver.effects, new ShipValueDiff(ship3, "shield", -30), "ship3 shield value"); + check.contains(maneuver.effects, new ShipValueDiff(ship3, "hull", -20), "ship3 hull value"); + check.contains(maneuver.effects, new ShipDamageDiff(ship2, 0, 50, 0, 50), "ship2 damage"); + check.contains(maneuver.effects, new ShipDamageDiff(ship3, 20, 30, 0, 50), "ship3 damage"); + }); +}); diff --git a/src/core/ai/Maneuver.ts b/src/core/ai/Maneuver.ts index 73a58eb..e331fc2 100644 --- a/src/core/ai/Maneuver.ts +++ b/src/core/ai/Maneuver.ts @@ -1,112 +1,119 @@ -module TK.SpaceTac { - /** - * Ship maneuver for an artifical intelligence - * - * A maneuver is like a human player action, choosing an action and using it - */ - export class Maneuver { - // Concerned ship - ship: Ship +import { any, nn } from "../../common/Tools" +import { BaseAction } from "../actions/BaseAction" +import { EndTurnAction } from "../actions/EndTurnAction" +import { Battle } from "../Battle" +import { BaseBattleDiff } from "../diffs/BaseBattleDiff" +import { MoveFireResult, MoveFireSimulator } from "../MoveFireSimulator" +import { Ship } from "../Ship" +import { Target } from "../Target" - // Reference to battle - battle: Battle +/** + * Ship maneuver for an artifical intelligence + * + * A maneuver is like a human player action, choosing an action and using it + */ +export class Maneuver { + // Concerned ship + ship: Ship - // Action to use - action: BaseAction + // Reference to battle + battle: Battle - // Target for the action - target: Target + // Action to use + action: BaseAction - // Result of move-fire simulation - simulation: MoveFireResult + // Target for the action + target: Target - // List of guessed effects of this maneuver (lazy property) - _effects?: BaseBattleDiff[] + // Result of move-fire simulation + simulation: MoveFireResult - constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) { - this.ship = ship; - this.battle = nn(ship.getBattle()); - this.action = action; - this.target = target; + // List of guessed effects of this maneuver (lazy property) + _effects?: BaseBattleDiff[] - let simulator = new MoveFireSimulator(this.ship); - this.simulation = simulator.simulateAction(this.action, this.target, move_margin); - } + constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) { + this.ship = ship; + this.battle = nn(ship.getBattle()); + this.action = action; + this.target = target; - jasmineToString() { - return `Use ${this.action.code} on ${this.target.jasmineToString()}`; - } + let simulator = new MoveFireSimulator(this.ship); + this.simulation = simulator.simulateAction(this.action, this.target, move_margin); + } - get effects(): BaseBattleDiff[] { - if (!this._effects) { - let simulator = new MoveFireSimulator(this.ship); - this._effects = simulator.getExpectedDiffs(this.battle, this.simulation); - } - return this._effects; - } + jasmineToString() { + return `Use ${this.action.code} on ${this.target.jasmineToString()}`; + } - /** - * Returns true if the maneuver has at least one part doable - */ - isPossible(): boolean { - return any(this.simulation.parts, part => part.possible); - } - - /** - * Returns true if the maneuver cannot be fully done this turn - */ - isIncomplete(): boolean { - return (this.simulation.need_move && !this.simulation.can_end_move) || (this.simulation.need_fire && !this.simulation.can_fire); - } - - /** - * Returns true if another maneuver could be done next on the same ship - */ - mayContinue(): boolean { - return this.ship.playing && !this.isIncomplete() && !(this.action instanceof EndTurnAction); - } - - /** - * Get the location of the ship after the action - */ - getFinalLocation(): { x: number, y: number } { - if (this.simulation.need_move) { - return this.simulation.move_location; - } else { - return { x: this.ship.arena_x, y: this.ship.arena_y }; - } - } - - /** - * Get the total power usage of this maneuver - */ - getPowerUsage(): number { - return this.simulation.total_move_ap + this.simulation.total_fire_ap; - } - - /** - * Standard feedback for this maneuver. It will apply it on the battle state. - */ - apply(battle: Battle): boolean { - if (!this.ship.is(battle.playing_ship)) { - console.error("Maneuver was not produced for the playing ship", this, battle); - return false; - } else if (!this.simulation.success) { - return false; - } else { - let parts = this.simulation.parts; - for (let i = 0; i < parts.length; i++) { - let part = parts[i]; - if (part.action instanceof EndTurnAction || part.possible) { - if (!battle.applyOneAction(part.action.id, part.target)) { - return false; - } - } else { - return false; - } - } - return this.mayContinue(); - } - } + get effects(): BaseBattleDiff[] { + if (!this._effects) { + let simulator = new MoveFireSimulator(this.ship); + this._effects = simulator.getExpectedDiffs(this.battle, this.simulation); } + return this._effects; + } + + /** + * Returns true if the maneuver has at least one part doable + */ + isPossible(): boolean { + return any(this.simulation.parts, part => part.possible); + } + + /** + * Returns true if the maneuver cannot be fully done this turn + */ + isIncomplete(): boolean { + return (this.simulation.need_move && !this.simulation.can_end_move) || (this.simulation.need_fire && !this.simulation.can_fire); + } + + /** + * Returns true if another maneuver could be done next on the same ship + */ + mayContinue(): boolean { + return this.ship.playing && !this.isIncomplete() && !(this.action instanceof EndTurnAction); + } + + /** + * Get the location of the ship after the action + */ + getFinalLocation(): { x: number, y: number } { + if (this.simulation.need_move) { + return this.simulation.move_location; + } else { + return { x: this.ship.arena_x, y: this.ship.arena_y }; + } + } + + /** + * Get the total power usage of this maneuver + */ + getPowerUsage(): number { + return this.simulation.total_move_ap + this.simulation.total_fire_ap; + } + + /** + * Standard feedback for this maneuver. It will apply it on the battle state. + */ + apply(battle: Battle): boolean { + if (!this.ship.is(battle.playing_ship)) { + console.error("Maneuver was not produced for the playing ship", this, battle); + return false; + } else if (!this.simulation.success) { + return false; + } else { + let parts = this.simulation.parts; + for (let i = 0; i < parts.length; i++) { + let part = parts[i]; + if (part.action instanceof EndTurnAction || part.possible) { + if (!battle.applyOneAction(part.action.id, part.target)) { + return false; + } + } else { + return false; + } + } + return this.mayContinue(); + } + } } diff --git a/src/core/ai/TacticalAI.spec.ts b/src/core/ai/TacticalAI.spec.ts index 4d880af..6be1e03 100644 --- a/src/core/ai/TacticalAI.spec.ts +++ b/src/core/ai/TacticalAI.spec.ts @@ -1,47 +1,45 @@ -module TK.SpaceTac.Specs { - testing("TacticalAI", test => { - class FixedManeuver extends Maneuver { - score: number; - constructor(score: number) { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - super(ship, new BaseAction("nothing"), new Target(0, 0)); - this.score = score; - } - } +testing("TacticalAI", test => { + class FixedManeuver extends Maneuver { + score: number; + constructor(score: number) { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + super(ship, new BaseAction("nothing"), new Target(0, 0)); + this.score = score; + } + } - // producer of FixedManeuver from a list of scores - let producer = (...scores: number[]) => imap(iarray(scores), score => new FixedManeuver(score)); - let applied: number[] = []; + // producer of FixedManeuver from a list of scores + let producer = (...scores: number[]) => imap(iarray(scores), score => new FixedManeuver(score)); + let applied: number[] = []; - test.setup(function () { - test.check.patch(console, "log", null); - applied = []; - }); + test.setup(function () { + test.check.patch(console, "log", null); + applied = []; + }); - test.case("applies the highest evaluated maneuver", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - TestTools.setShipPlaying(battle, ship); - ship.playing = true; + test.case("applies the highest evaluated maneuver", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + TestTools.setShipPlaying(battle, ship); + ship.playing = true; - let ai = new TacticalAI(ship, maneuver => { - if (maneuver instanceof FixedManeuver) { - applied.push(maneuver.score); - } - return false; - }, false, Timer.synchronous); + let ai = new TacticalAI(ship, maneuver => { + if (maneuver instanceof FixedManeuver) { + applied.push(maneuver.score); + } + return false; + }, false, Timer.synchronous); - check.patch(ai, "getDefaultProducers", () => [ - producer(1, -8, 4), - producer(3, 7, 0, 6, 1) - ]); - check.patch(ai, "getDefaultEvaluators", () => [ - (maneuver: Maneuver) => (maneuver).score - ]); + check.patch(ai, "getDefaultProducers", () => [ + producer(1, -8, 4), + producer(3, 7, 0, 6, 1) + ]); + check.patch(ai, "getDefaultEvaluators", () => [ + (maneuver: Maneuver) => (maneuver).score + ]); - ai.play(); - check.equals(applied, [7]); - }); - }); -} + ai.play(); + check.equals(applied, [7]); + }); +}); diff --git a/src/core/ai/TacticalAI.ts b/src/core/ai/TacticalAI.ts index 4eca9c3..c79ace3 100644 --- a/src/core/ai/TacticalAI.ts +++ b/src/core/ai/TacticalAI.ts @@ -1,135 +1,139 @@ -/// -/// -module TK.SpaceTac { +import { ialternate, IATEND } from "../../common/Iterators"; +import { nn, sum } from "../../common/Tools"; +import { EndTurnAction } from "../actions/EndTurnAction"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { AbstractAI } from "./AbstractAI"; +import { Maneuver } from "./Maneuver"; +import { TacticalAIHelpers } from "./TacticalAIHelpers"; - export type TacticalProducer = Iterable; - export type TacticalEvaluator = (maneuver: Maneuver) => number; +export type TacticalProducer = Iterable; +export type TacticalEvaluator = (maneuver: Maneuver) => number; - /** - * AI that applies a set of tactical rules - * - * It uses a set of producers (to propose new maneuvers), and evaluators (to choose the best maneuver). - * - * As much work as possible is done using iterators, without materializing every possibilities. - */ - export class TacticalAI extends AbstractAI { - private producers: TacticalProducer[] = [] - private work: Iterator = IATEND - private evaluators: TacticalEvaluator[] = [] +/** + * AI that applies a set of tactical rules + * + * It uses a set of producers (to propose new maneuvers), and evaluators (to choose the best maneuver). + * + * As much work as possible is done using iterators, without materializing every possibilities. + */ +export class TacticalAI extends AbstractAI { + private producers: TacticalProducer[] = [] + private work: Iterator = IATEND + private evaluators: TacticalEvaluator[] = [] - private best: Maneuver | null = null - private best_score = 0 - private produced = 0 - private evaluated = 0 + private best: Maneuver | null = null + private best_score = 0 + private produced = 0 + private evaluated = 0 - protected initWork(): void { - this.best = null; - this.best_score = -Infinity; + protected initWork(): void { + this.best = null; + this.best_score = -Infinity; - this.producers = this.getDefaultProducers(); - this.work = ialternate(this.producers)[Symbol.iterator](); - this.evaluators = this.getDefaultEvaluators(); - this.produced = 0; - this.evaluated = 0; + this.producers = this.getDefaultProducers(); + this.work = ialternate(this.producers)[Symbol.iterator](); + this.evaluators = this.getDefaultEvaluators(); + this.produced = 0; + this.evaluated = 0; - if (this.debug) { - console.log("AI started", this.name, this.ship.name); - } - } - - protected doWorkUnit(): boolean { - let state = this.work.next(); - - if (!state.done && this.getDuration() < 8000) { - let maneuver = state.value; - this.produced++; - if (maneuver.isPossible()) { - // Evaluate the maneuver - let score = this.evaluate(maneuver); - this.evaluated++; - if (this.debug) { - console.debug("AI evaluation", maneuver, score); - } - if ((Math.abs(score - this.best_score) < 0.0001 && this.random.bool()) || score > this.best_score) { - this.best = maneuver; - this.best_score = score; - } - } - - return true; - } else if (this.best) { - if (!state.done) { - console.warn(`AI did not analyze every possible maneuver (${this.produced} produced, ${this.evaluated} evaluated)`); - } - - // Choose the best maneuver so far - let best_maneuver = this.best; - if (this.debug) { - console.log("AI maneuver", this.name, this.ship.name, best_maneuver, this.best_score); - } - - if (this.best.action instanceof EndTurnAction) { - return false; - } - - let success = this.feedback(best_maneuver); - if (success) { - // Try to play another maneuver - this.initWork(); - return true; - } else { - return false; - } - } else { - // No maneuver produced - return false; - } - } - - /** - * Evaluate a single maneuver - */ - evaluate(maneuver: Maneuver) { - return sum(this.evaluators.map(evaluator => evaluator(maneuver))); - } - - /** - * Get the default set of maneuver producers - */ - getDefaultProducers() { - let producers = [ - TacticalAIHelpers.produceEndTurn, - TacticalAIHelpers.produceDirectShots, - TacticalAIHelpers.produceBlastShots, - TacticalAIHelpers.produceToggleActions, - TacticalAIHelpers.produceRandomMoves, - ] - return producers.map(producer => producer(this.ship, this.ship.getBattle() || new Battle())); - } - - /** - * Get the default set of maneuver evaluators - */ - getDefaultEvaluators() { - type EvaluatorHelper = (ship: Ship, battle: Battle, maneuver: Maneuver) => number; - - function scaled(evaluator: EvaluatorHelper, factor: number): EvaluatorHelper { - return (ship: Ship, battle: Battle, maneuver: Maneuver) => factor * evaluator(ship, battle, maneuver); - } - - let evaluators: EvaluatorHelper[] = [ - scaled(TacticalAIHelpers.evaluateTurnCost, 1), - scaled(TacticalAIHelpers.evaluateOverheat, 3), - scaled(TacticalAIHelpers.evaluateEnemyHealth, 5), - scaled(TacticalAIHelpers.evaluateAllyHealth, 20), - scaled(TacticalAIHelpers.evaluateActiveEffects, 3), - scaled(TacticalAIHelpers.evaluateClustering, 4), - scaled(TacticalAIHelpers.evaluatePosition, 0.5), - scaled(TacticalAIHelpers.evaluateIdling, 2), - ] - - let battle = nn(this.ship.getBattle()); - return evaluators.map(evaluator => ((maneuver: Maneuver) => evaluator(this.ship, battle, maneuver))); - } + if (this.debug) { + console.log("AI started", this.name, this.ship.name); } + } + + protected doWorkUnit(): boolean { + let state = this.work.next(); + + if (!state.done && this.getDuration() < 8000) { + let maneuver = state.value; + this.produced++; + if (maneuver.isPossible()) { + // Evaluate the maneuver + let score = this.evaluate(maneuver); + this.evaluated++; + if (this.debug) { + console.debug("AI evaluation", maneuver, score); + } + if ((Math.abs(score - this.best_score) < 0.0001 && this.random.bool()) || score > this.best_score) { + this.best = maneuver; + this.best_score = score; + } + } + + return true; + } else if (this.best) { + if (!state.done) { + console.warn(`AI did not analyze every possible maneuver (${this.produced} produced, ${this.evaluated} evaluated)`); + } + + // Choose the best maneuver so far + let best_maneuver = this.best; + if (this.debug) { + console.log("AI maneuver", this.name, this.ship.name, best_maneuver, this.best_score); + } + + if (this.best.action instanceof EndTurnAction) { + return false; + } + + let success = this.feedback(best_maneuver); + if (success) { + // Try to play another maneuver + this.initWork(); + return true; + } else { + return false; + } + } else { + // No maneuver produced + return false; + } + } + + /** + * Evaluate a single maneuver + */ + evaluate(maneuver: Maneuver) { + return sum(this.evaluators.map(evaluator => evaluator(maneuver))); + } + + /** + * Get the default set of maneuver producers + */ + getDefaultProducers() { + let producers = [ + TacticalAIHelpers.produceEndTurn, + TacticalAIHelpers.produceDirectShots, + TacticalAIHelpers.produceBlastShots, + TacticalAIHelpers.produceToggleActions, + TacticalAIHelpers.produceRandomMoves, + ] + return producers.map(producer => producer(this.ship, this.ship.getBattle() || new Battle())); + } + + /** + * Get the default set of maneuver evaluators + */ + getDefaultEvaluators() { + type EvaluatorHelper = (ship: Ship, battle: Battle, maneuver: Maneuver) => number; + + function scaled(evaluator: EvaluatorHelper, factor: number): EvaluatorHelper { + return (ship: Ship, battle: Battle, maneuver: Maneuver) => factor * evaluator(ship, battle, maneuver); + } + + let evaluators: EvaluatorHelper[] = [ + scaled(TacticalAIHelpers.evaluateTurnCost, 1), + scaled(TacticalAIHelpers.evaluateOverheat, 3), + scaled(TacticalAIHelpers.evaluateEnemyHealth, 5), + scaled(TacticalAIHelpers.evaluateAllyHealth, 20), + scaled(TacticalAIHelpers.evaluateActiveEffects, 3), + scaled(TacticalAIHelpers.evaluateClustering, 4), + scaled(TacticalAIHelpers.evaluatePosition, 0.5), + scaled(TacticalAIHelpers.evaluateIdling, 2), + ] + + let battle = nn(this.ship.getBattle()); + return evaluators.map(evaluator => ((maneuver: Maneuver) => evaluator(this.ship, battle, maneuver))); + } } diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts index 8680e87..9952020 100644 --- a/src/core/ai/TacticalAIHelpers.spec.ts +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -1,313 +1,311 @@ -module TK.SpaceTac.Specs { - testing("TacticalAIHelpers", test => { - test.case("produces direct weapon shots", check => { - let battle = new Battle(); - let ship0a = battle.fleets[0].addShip(new Ship(null, "0A")); - let ship0b = battle.fleets[0].addShip(new Ship(null, "0B")); - let ship1a = battle.fleets[1].addShip(new Ship(null, "1A")); - let ship1b = battle.fleets[1].addShip(new Ship(null, "1B")); - - TestTools.setShipModel(ship0a, 100, 0, 10); - TestTools.setShipPlaying(battle, ship0a); - - let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); - check.equals(result.length, 0); - - let weapon1 = TestTools.addWeapon(ship0a, 10); - let weapon2 = TestTools.addWeapon(ship0a, 15); - result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); - check.equals(result.length, 4); - check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a))); - check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b))); - check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a))); - check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b))); - }); - - test.case("produces random moves inside a grid", check => { - let battle = new Battle(); - battle.width = 100; - battle.height = 100; - let ship = battle.fleets[0].addShip(); - - TestTools.setShipModel(ship, 100, 0, 10); - TestTools.setShipPlaying(battle, ship); - - let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1)); - check.equals(result.length, 0); - - let engine = TestTools.addEngine(ship, 1000); - - result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true))); - check.equals(result, [ - new Maneuver(ship, engine, Target.newFromLocation(25, 25)), - new Maneuver(ship, engine, Target.newFromLocation(75, 25)), - new Maneuver(ship, engine, Target.newFromLocation(25, 75)), - new Maneuver(ship, engine, Target.newFromLocation(75, 75)), - ]); - }); - - test.case("produces interesting blast shots", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); - TestTools.setShipModel(ship, 100, 0, 10, 1, [weapon]); - TestTools.setShipPlaying(battle, ship); - - let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); - check.equals(result.length, 0); - - let enemy1 = battle.fleets[1].addShip(); - enemy1.setArenaPosition(500, 0); - - result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); - check.equals(result.length, 0); - - let enemy2 = battle.fleets[1].addShip(); - enemy2.setArenaPosition(700, 0); - - result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); - check.equals(result, [ - new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), - new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), - ]); - - let enemy3 = battle.fleets[1].addShip(); - enemy3.setArenaPosition(700, 300); - - result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); - check.equals(result, [ - new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), - new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), - ]); - }); - - test.case("produces toggle/untoggle actions", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let action1 = new DeployDroneAction("Drone"); - let action2 = new ToggleAction("Toggle"); - let action3 = new VigilanceAction("Vigilance", { radius: 150 }); - TestTools.setShipModel(ship, 100, 0, 10, 1, [action1, action2, action3]); - TestTools.addEngine(ship, 1000); - TestTools.setShipPlaying(battle, ship); - - check.patch(TacticalAIHelpers, "scanArena", () => iarray([ - Target.newFromLocation(1, 0), - Target.newFromLocation(0, 1), - ])); - - let result = imaterialize(TacticalAIHelpers.produceToggleActions(ship, battle)); - check.equals(result, [ - new Maneuver(ship, action2, Target.newFromShip(ship)), - new Maneuver(ship, action1, Target.newFromLocation(1, 0)), - new Maneuver(ship, action3, Target.newFromLocation(1, 0)), - new Maneuver(ship, action1, Target.newFromLocation(0, 1)), - new Maneuver(ship, action3, Target.newFromLocation(0, 1)), - ]); - }); - - test.case("evaluates turn cost", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let weapon = TestTools.addWeapon(ship, 50, 5, 100); - let action = weapon; - let engine = TestTools.addEngine(ship, 25); - - let maneuver = new Maneuver(ship, new BaseAction("fake"), new Target(0, 0), 0); - check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -1); - - maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); - check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); - - TestTools.setShipModel(ship, 100, 0, 4, 1, [engine, action]); - maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); - check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); - - TestTools.setShipModel(ship, 100, 0, 10, 1, [engine, action]); - maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); - check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.5); // 5 power remaining on 10 - - maneuver = new Maneuver(ship, action, Target.newFromLocation(110, 0), 0); - check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.4); // 4 power remaining on 10 - - maneuver = new Maneuver(ship, action, Target.newFromLocation(140, 0), 0); - check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.3); // 3 power remaining on 10 - - maneuver = new Maneuver(ship, action, Target.newFromLocation(310, 0), 0); - check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -1); // can't do in one turn - }); - - test.case("evaluates the drawback of doing nothing", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - TestTools.setShipModel(ship, 100, 0, 10); - let engine = TestTools.addEngine(ship, 50); - let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10); - let toggle = ship.actions.addCustom(new ToggleAction("test")); - - let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(0, 0)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5, "fire"); - - maneuver = new Maneuver(ship, toggle, Target.newFromShip(ship)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5, "toggle on"); - - ship.actions.toggle(toggle, true); - maneuver = new Maneuver(ship, toggle, Target.newFromShip(ship)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.2, "toggle off"); - - maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 48)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.9, "move only, at full power"); - - maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -1, "end turn, at full power"); - - ship.setValue("power", 2); - - maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 48)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.1, "move only, at reduced power"); - - maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.2, "end turn, at reduced power"); - }); - - test.case("evaluates damage to enemies", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let action = TestTools.addWeapon(ship, 50, 5, 500, 100); - - let enemy1 = battle.fleets[1].addShip(); - enemy1.setArenaPosition(250, 0); - TestTools.setShipModel(enemy1, 50, 25); - let enemy2 = battle.fleets[1].addShip(); - enemy2.setArenaPosition(300, 0); - TestTools.setShipModel(enemy2, 25, 0); - - // no enemies hurt - let maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0)); - check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0, 8); - - // one enemy loses half-life - maneuver = new Maneuver(ship, action, Target.newFromLocation(180, 0)); - check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0.1666666666, 8); - - // one enemy loses half-life, the other one is dead - maneuver = new Maneuver(ship, action, Target.newFromLocation(280, 0)); - check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0.6666666666, 8); - }); - - test.case("evaluates ship clustering", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - TestTools.setShipModel(ship, 100, 0, 10); - TestTools.addEngine(ship, 100); - let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10); - - let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(200, 0), 0.5); - check.nears(maneuver.simulation.move_location.x, 100.5, 1); - check.equals(maneuver.simulation.move_location.y, 0); - check.equals(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), 0); - - battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height); - check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.01, 2); - - battle.fleets[1].addShip().setArenaPosition(120, 40); - check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.4, 1); - - battle.fleets[0].addShip().setArenaPosition(80, 60); - check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.7, 1); - - battle.fleets[0].addShip().setArenaPosition(110, 20); - check.equals(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -1); - }); - - test.case("evaluates ship position", check => { - let battle = new Battle(undefined, undefined, 200, 100); - let ship = battle.fleets[0].addShip(); - let weapon = TestTools.addWeapon(ship, 1, 1, 400); - let action = weapon; - - ship.setArenaPosition(0, 0); - let maneuver = new Maneuver(ship, action, new Target(0, 0), 0); - check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -1); - - ship.setArenaPosition(100, 0); - maneuver = new Maneuver(ship, action, new Target(0, 0), 0); - check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -1); - - ship.setArenaPosition(100, 10); - maneuver = new Maneuver(ship, action, new Target(0, 0), 0); - check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -0.6); - - ship.setArenaPosition(100, 50); - maneuver = new Maneuver(ship, action, new Target(0, 0), 0); - check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), 1); - }); - - test.case("evaluates overheat", check => { - let battle = new Battle(undefined, undefined, 200, 100); - let ship = battle.fleets[0].addShip(); - let weapon = TestTools.addWeapon(ship, 1, 1, 400); - - let maneuver = new Maneuver(ship, weapon, new Target(0, 0)); - check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); - - weapon.configureCooldown(1, 1); - ship.actions.updateFromShip(ship); - ship.actions.addCustom(weapon); - check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.4); - - weapon.configureCooldown(1, 2); - ship.actions.updateFromShip(ship); - ship.actions.addCustom(weapon); - check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.8); - - weapon.configureCooldown(1, 3); - ship.actions.updateFromShip(ship); - ship.actions.addCustom(weapon); - check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -1); - - weapon.configureCooldown(2, 1); - ship.actions.updateFromShip(ship); - ship.actions.addCustom(weapon); - check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); - }); - - test.case("evaluates active effects", check => { - let battle = TestTools.createBattle(); - let ship = battle.fleets[0].ships[0]; - let enemy = battle.fleets[1].ships[0]; - TestTools.setShipModel(ship, 1, 0, 1); - TestTools.setShipModel(enemy, 5, 5); - let action = new TriggerAction("Test", { range: 100, power: 1 }); - ship.actions.addCustom(action); - - let maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); - check.equals(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0); - - action.effects = [new StickyEffect(new DamageEffect(1), 1)]; - maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5); - - maneuver = new Maneuver(ship, action, Target.newFromShip(ship)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5); - - action.effects = [new StickyEffect(new CooldownEffect(1), 1)]; - maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5); - - maneuver = new Maneuver(ship, action, Target.newFromShip(ship)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5); - - battle.fleets[0].addShip(); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.3333333333333333); - - action.effects = [new StickyEffect(new CooldownEffect(1), 1), new StickyEffect(new CooldownEffect(1), 1)]; - maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.6666666666666666); - - action.effects = range(10).map(() => new StickyEffect(new CooldownEffect(1), 1)); - maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); - check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -1); - }); - }); -} +testing("TacticalAIHelpers", test => { + test.case("produces direct weapon shots", check => { + let battle = new Battle(); + let ship0a = battle.fleets[0].addShip(new Ship(null, "0A")); + let ship0b = battle.fleets[0].addShip(new Ship(null, "0B")); + let ship1a = battle.fleets[1].addShip(new Ship(null, "1A")); + let ship1b = battle.fleets[1].addShip(new Ship(null, "1B")); + + TestTools.setShipModel(ship0a, 100, 0, 10); + TestTools.setShipPlaying(battle, ship0a); + + let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); + check.equals(result.length, 0); + + let weapon1 = TestTools.addWeapon(ship0a, 10); + let weapon2 = TestTools.addWeapon(ship0a, 15); + result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); + check.equals(result.length, 4); + check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a))); + check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b))); + check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a))); + check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b))); + }); + + test.case("produces random moves inside a grid", check => { + let battle = new Battle(); + battle.width = 100; + battle.height = 100; + let ship = battle.fleets[0].addShip(); + + TestTools.setShipModel(ship, 100, 0, 10); + TestTools.setShipPlaying(battle, ship); + + let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1)); + check.equals(result.length, 0); + + let engine = TestTools.addEngine(ship, 1000); + + result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true))); + check.equals(result, [ + new Maneuver(ship, engine, Target.newFromLocation(25, 25)), + new Maneuver(ship, engine, Target.newFromLocation(75, 25)), + new Maneuver(ship, engine, Target.newFromLocation(25, 75)), + new Maneuver(ship, engine, Target.newFromLocation(75, 75)), + ]); + }); + + test.case("produces interesting blast shots", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); + TestTools.setShipModel(ship, 100, 0, 10, 1, [weapon]); + TestTools.setShipPlaying(battle, ship); + + let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); + check.equals(result.length, 0); + + let enemy1 = battle.fleets[1].addShip(); + enemy1.setArenaPosition(500, 0); + + result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); + check.equals(result.length, 0); + + let enemy2 = battle.fleets[1].addShip(); + enemy2.setArenaPosition(700, 0); + + result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); + check.equals(result, [ + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + ]); + + let enemy3 = battle.fleets[1].addShip(); + enemy3.setArenaPosition(700, 300); + + result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); + check.equals(result, [ + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + ]); + }); + + test.case("produces toggle/untoggle actions", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let action1 = new DeployDroneAction("Drone"); + let action2 = new ToggleAction("Toggle"); + let action3 = new VigilanceAction("Vigilance", { radius: 150 }); + TestTools.setShipModel(ship, 100, 0, 10, 1, [action1, action2, action3]); + TestTools.addEngine(ship, 1000); + TestTools.setShipPlaying(battle, ship); + + check.patch(TacticalAIHelpers, "scanArena", () => iarray([ + Target.newFromLocation(1, 0), + Target.newFromLocation(0, 1), + ])); + + let result = imaterialize(TacticalAIHelpers.produceToggleActions(ship, battle)); + check.equals(result, [ + new Maneuver(ship, action2, Target.newFromShip(ship)), + new Maneuver(ship, action1, Target.newFromLocation(1, 0)), + new Maneuver(ship, action3, Target.newFromLocation(1, 0)), + new Maneuver(ship, action1, Target.newFromLocation(0, 1)), + new Maneuver(ship, action3, Target.newFromLocation(0, 1)), + ]); + }); + + test.case("evaluates turn cost", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 50, 5, 100); + let action = weapon; + let engine = TestTools.addEngine(ship, 25); + + let maneuver = new Maneuver(ship, new BaseAction("fake"), new Target(0, 0), 0); + check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -1); + + maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); + check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); + + TestTools.setShipModel(ship, 100, 0, 4, 1, [engine, action]); + maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); + check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); + + TestTools.setShipModel(ship, 100, 0, 10, 1, [engine, action]); + maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); + check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.5); // 5 power remaining on 10 + + maneuver = new Maneuver(ship, action, Target.newFromLocation(110, 0), 0); + check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.4); // 4 power remaining on 10 + + maneuver = new Maneuver(ship, action, Target.newFromLocation(140, 0), 0); + check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.3); // 3 power remaining on 10 + + maneuver = new Maneuver(ship, action, Target.newFromLocation(310, 0), 0); + check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -1); // can't do in one turn + }); + + test.case("evaluates the drawback of doing nothing", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 100, 0, 10); + let engine = TestTools.addEngine(ship, 50); + let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10); + let toggle = ship.actions.addCustom(new ToggleAction("test")); + + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(0, 0)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5, "fire"); + + maneuver = new Maneuver(ship, toggle, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5, "toggle on"); + + ship.actions.toggle(toggle, true); + maneuver = new Maneuver(ship, toggle, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.2, "toggle off"); + + maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 48)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.9, "move only, at full power"); + + maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -1, "end turn, at full power"); + + ship.setValue("power", 2); + + maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 48)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.1, "move only, at reduced power"); + + maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.2, "end turn, at reduced power"); + }); + + test.case("evaluates damage to enemies", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let action = TestTools.addWeapon(ship, 50, 5, 500, 100); + + let enemy1 = battle.fleets[1].addShip(); + enemy1.setArenaPosition(250, 0); + TestTools.setShipModel(enemy1, 50, 25); + let enemy2 = battle.fleets[1].addShip(); + enemy2.setArenaPosition(300, 0); + TestTools.setShipModel(enemy2, 25, 0); + + // no enemies hurt + let maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0)); + check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0, 8); + + // one enemy loses half-life + maneuver = new Maneuver(ship, action, Target.newFromLocation(180, 0)); + check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0.1666666666, 8); + + // one enemy loses half-life, the other one is dead + maneuver = new Maneuver(ship, action, Target.newFromLocation(280, 0)); + check.nears(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver), 0.6666666666, 8); + }); + + test.case("evaluates ship clustering", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 100, 0, 10); + TestTools.addEngine(ship, 100); + let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10); + + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(200, 0), 0.5); + check.nears(maneuver.simulation.move_location.x, 100.5, 1); + check.equals(maneuver.simulation.move_location.y, 0); + check.equals(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), 0); + + battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height); + check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.01, 2); + + battle.fleets[1].addShip().setArenaPosition(120, 40); + check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.4, 1); + + battle.fleets[0].addShip().setArenaPosition(80, 60); + check.nears(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -0.7, 1); + + battle.fleets[0].addShip().setArenaPosition(110, 20); + check.equals(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), -1); + }); + + test.case("evaluates ship position", check => { + let battle = new Battle(undefined, undefined, 200, 100); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 1, 1, 400); + let action = weapon; + + ship.setArenaPosition(0, 0); + let maneuver = new Maneuver(ship, action, new Target(0, 0), 0); + check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -1); + + ship.setArenaPosition(100, 0); + maneuver = new Maneuver(ship, action, new Target(0, 0), 0); + check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -1); + + ship.setArenaPosition(100, 10); + maneuver = new Maneuver(ship, action, new Target(0, 0), 0); + check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), -0.6); + + ship.setArenaPosition(100, 50); + maneuver = new Maneuver(ship, action, new Target(0, 0), 0); + check.equals(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver), 1); + }); + + test.case("evaluates overheat", check => { + let battle = new Battle(undefined, undefined, 200, 100); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 1, 1, 400); + + let maneuver = new Maneuver(ship, weapon, new Target(0, 0)); + check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); + + weapon.configureCooldown(1, 1); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); + check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.4); + + weapon.configureCooldown(1, 2); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); + check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.8); + + weapon.configureCooldown(1, 3); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); + check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -1); + + weapon.configureCooldown(2, 1); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); + check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); + }); + + test.case("evaluates active effects", check => { + let battle = TestTools.createBattle(); + let ship = battle.fleets[0].ships[0]; + let enemy = battle.fleets[1].ships[0]; + TestTools.setShipModel(ship, 1, 0, 1); + TestTools.setShipModel(enemy, 5, 5); + let action = new TriggerAction("Test", { range: 100, power: 1 }); + ship.actions.addCustom(action); + + let maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); + check.equals(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0); + + action.effects = [new StickyEffect(new DamageEffect(1), 1)]; + maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5); + + maneuver = new Maneuver(ship, action, Target.newFromShip(ship)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5); + + action.effects = [new StickyEffect(new CooldownEffect(1), 1)]; + maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5); + + maneuver = new Maneuver(ship, action, Target.newFromShip(ship)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5); + + battle.fleets[0].addShip(); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.3333333333333333); + + action.effects = [new StickyEffect(new CooldownEffect(1), 1), new StickyEffect(new CooldownEffect(1), 1)]; + maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.6666666666666666); + + action.effects = range(10).map(() => new StickyEffect(new CooldownEffect(1), 1)); + maneuver = new Maneuver(ship, action, Target.newFromShip(enemy)); + check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -1); + }); +}); diff --git a/src/core/ai/TacticalAIHelpers.ts b/src/core/ai/TacticalAIHelpers.ts index ac56688..ffc6484 100644 --- a/src/core/ai/TacticalAIHelpers.ts +++ b/src/core/ai/TacticalAIHelpers.ts @@ -1,258 +1,272 @@ -module TK.SpaceTac { - /** - * Get a list of all playable actions (like the actionbar for player) for a ship - */ - function getPlayableActions(ship: Ship): Iterable { - let actions = ship.actions.listAll(); - return ifilter(iarray(actions), action => !action.checkCannotBeApplied(ship)); +import { iarray, ichain, ichainit, icombine, ifilter, ifilterclass, imap, imaterialize, irange, isingle } from "../../common/Iterators"; +import { RandomGenerator } from "../../common/RandomGenerator"; +import { clamp, contains, max, min, sum } from "../../common/Tools"; +import { ActionTargettingMode, BaseAction } from "../actions/BaseAction"; +import { EndTurnAction } from "../actions/EndTurnAction"; +import { MoveAction } from "../actions/MoveAction"; +import { ToggleAction } from "../actions/ToggleAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { Battle } from "../Battle"; +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "../diffs/ShipEffectAddedDiff"; +import { ShipValueDiff } from "../diffs/ShipValueDiff"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { Maneuver } from "./Maneuver"; +import { TacticalProducer } from "./TacticalAI"; + +/** + * Get a list of all playable actions (like the actionbar for player) for a ship + */ +function getPlayableActions(ship: Ship): Iterable { + let actions = ship.actions.listAll(); + return ifilter(iarray(actions), action => !action.checkCannotBeApplied(ship)); +} + +/** + * Get the proportional effect done to a ship's health (in -1,1 range) + */ +function getProportionalHealth(maneuver: Maneuver, ship: Ship): number { + let chull = ship.getAttribute("hull_capacity"); + let cshield = ship.getAttribute("shield_capacity"); + let hull = ship.getValue("hull") + let shield = ship.getValue("shield"); + let dhull = 0; + let dshield = 0; + + maneuver.effects.forEach(diff => { + if (diff instanceof ShipValueDiff) { + if (ship.is(diff.ship_id)) { + if (diff.code == "hull") { + dhull += clamp(hull + diff.diff, 0, chull) - hull; + } else if (diff.code == "shield") { + dshield += clamp(shield + diff.diff, 0, cshield) - shield; + } + } } + }); - /** - * Get the proportional effect done to a ship's health (in -1,1 range) - */ - function getProportionalHealth(maneuver: Maneuver, ship: Ship): number { - let chull = ship.getAttribute("hull_capacity"); - let cshield = ship.getAttribute("shield_capacity"); - let hull = ship.getValue("hull") - let shield = ship.getValue("shield"); - let dhull = 0; - let dshield = 0; + if (hull + dhull <= 0) { + return -1; + } else { + let diff = dhull + dshield; + return clamp(diff / (hull + shield), -1, 1); + } +} - maneuver.effects.forEach(diff => { - if (diff instanceof ShipValueDiff) { - if (ship.is(diff.ship_id)) { - if (diff.code == "hull") { - dhull += clamp(hull + diff.diff, 0, chull) - hull; - } else if (diff.code == "shield") { - dshield += clamp(shield + diff.diff, 0, cshield) - shield; - } - } - } - }); +/** + * Standard producers and evaluators for TacticalAI + * + * These are static methods that may be used as base for TacticalAI ruleset. + */ +export class TacticalAIHelpers { + /** + * Iterator of a list of "random" arena coordinates, based on a grid + */ + static scanArena(battle: Battle, cells = 10, random = RandomGenerator.global): Iterable { + return imap(irange(cells * cells), cellpos => { + let y = Math.floor(cellpos / cells); + let x = cellpos - y * cells; + return Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells); + }); + } - if (hull + dhull <= 0) { - return -1; + /** + * Produce a turn end. + */ + static produceEndTurn(ship: Ship, battle: Battle): TacticalProducer { + return isingle(new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); + } + + /** + * Produce all "direct hit" weapon shots. + */ + static produceDirectShots(ship: Ship, battle: Battle): TacticalProducer { + let enemies = battle.ienemies(ship, true); + let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction); + return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy))); + } + + /** + * Produce random moves inside arena cell + */ + static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer { + let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction); + return ichainit(imap(irange(iterations), iteration => { + let moves = icombine(engines, TacticalAIHelpers.scanArena(battle, cells, random)); + return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target)); + })); + } + + /** + * Produce blast weapon shots, with multiple targets. + */ + static produceInterestingBlastShots(ship: Ship, battle: Battle): TacticalProducer { + // TODO Work with groups of 3, 4 ... + let weapons = ifilter(ifilterclass(getPlayableActions(ship), TriggerAction), action => action.blast > 0); + let enemies = battle.ienemies(ship, true); + // TODO This produces duplicates (x, y) and (y, x) + let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2); + let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.blast * 2); + let result = imap(candidates, ([weapon, [e1, e2]]) => new Maneuver(ship, weapon, Target.newFromLocation((e1.arena_x + e2.arena_x) / 2, (e1.arena_y + e2.arena_y) / 2))); + return result; + } + + /** + * Produce random blast weapon shots, on a grid. + */ + static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer { + let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0); + let candidates = ifilter(icombine(weapons, TacticalAIHelpers.scanArena(battle)), ([weapon, location]) => (weapon).getEffects(ship, location).length > 0); + let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location)); + return result; + } + + /** + * Produce interesting then random blast shots + */ + static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer { + return ichain(TacticalAIHelpers.produceInterestingBlastShots(ship, battle), TacticalAIHelpers.produceRandomBlastShots(ship, battle)); + } + + /** + * Produce toggle actions at random locations. + */ + static produceToggleActions(ship: Ship, battle: Battle): TacticalProducer { + let toggles = ifilter(getPlayableActions(ship), action => action instanceof ToggleAction); + + let self_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SELF_CONFIRM, ActionTargettingMode.SELF], toggle.getTargettingMode(ship))); + let self_maneuvers = imap(self_toggles, toggle => new Maneuver(ship, toggle, Target.newFromShip(ship))); + + let distant_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SPACE, ActionTargettingMode.SURROUNDINGS], toggle.getTargettingMode(ship))); + let grid = TacticalAIHelpers.scanArena(battle); + let distant_maneuvers = imap(icombine(grid, distant_toggles), ([location, toggle]) => new Maneuver(ship, toggle, location)); + + return ichain(self_maneuvers, distant_maneuvers); + } + + /** + * Evaluate the number of turns necessary for the maneuver, between -1 and 1 + */ + static evaluateTurnCost(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let powerusage = maneuver.simulation.total_move_ap + maneuver.simulation.total_fire_ap; + if (powerusage == 0) { + return -1; + } else if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) { + return -Infinity; + } else if (powerusage > ship.getValue("power")) { + return -1; + } else { + return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity"); + } + } + + /** + * Evaluate doing nothing, between -1 and 1 + */ + static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let power_capacity = ship.getAttribute("power_capacity") || 1; + + if (maneuver.action instanceof EndTurnAction) { + return -ship.getValue("power") / power_capacity; + } else if (maneuver.action instanceof TriggerAction) { + return 0.5; + } else if (maneuver.action instanceof ToggleAction) { + return ship.actions.isToggled(maneuver.action) ? -0.2 : 0.5; + } else if (maneuver.action instanceof MoveAction) { + return -(ship.getValue("power") - maneuver.getPowerUsage()) / power_capacity; + } else { + return 0; + } + } + + /** + * Evaluate the effect on health for a group of ships + */ + static evaluateHealthEffect(maneuver: Maneuver, ships: Ship[]): number { + if (ships.length) { + let diffs = ships.map(ship => getProportionalHealth(maneuver, ship)); + let deaths = sum(diffs.map(i => i == -1 ? 1 : 0)); + return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length; + } else { + return 0; + } + } + + /** + * Evaluate the effect on health to the enemy, between -1 and 1 + */ + static evaluateEnemyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let enemies = imaterialize(battle.ienemies(ship, true)); + return -TacticalAIHelpers.evaluateHealthEffect(maneuver, enemies); + } + + /** + * Evaluate the effect on health to allied ships, between -1 and 1 + */ + static evaluateAllyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let allies = imaterialize(battle.iallies(ship, true)); + return TacticalAIHelpers.evaluateHealthEffect(maneuver, allies); + } + + /** + * Evaluate the clustering of ships, between -1 and 1 + */ + static evaluateClustering(ship: Ship, battle: Battle, maneuver: Maneuver): number { + // TODO Take into account blast radius of in-play weapons + let move_location = maneuver.getFinalLocation(); + let distances = imaterialize(imap(ifilter(battle.iships(), iship => iship != ship), iship => Target.newFromShip(iship).getDistanceTo(move_location))); + if (distances.length == 0) { + return 0; + } else { + let factor = max([battle.width, battle.height]) * 0.01; + let result = -clamp(sum(distances.map(distance => factor / distance)), 0, 1); + return result; + } + } + + /** + * Evaluate the global positioning of a ship on the arena, between -1 and 1 + */ + static evaluatePosition(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let pos = maneuver.getFinalLocation(); + let distance = min([pos.x, pos.y, battle.width - pos.x, battle.height - pos.y]); + let factor = min([battle.width / 2, battle.height / 2]); + return -1 + 2 * distance / factor; + } + + /** + * Evaluate the cost of overheating an equipment + */ + static evaluateOverheat(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let cooldown = ship.actions.getCooldown(maneuver.action); + if (cooldown.willOverheat()) { + return -Math.min(1, 0.4 * cooldown.cooling); + } else { + return 0; + } + } + + /** + * Evaluate the gain or loss of active effects + */ + static evaluateActiveEffects(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let result = 0; + maneuver.effects.forEach(effect => { + if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) { + let target = battle.getShip(effect.ship_id); + let enemy = target && !target.isPlayedBy(ship.getPlayer()); + let beneficial = effect.effect.isBeneficial(); + if (effect instanceof ShipEffectRemovedDiff) { + beneficial = !beneficial; + } + // TODO Evaluate the "power" of the effect + if ((beneficial && !enemy) || (!beneficial && enemy)) { + result += 1; } else { - let diff = dhull + dshield; - return clamp(diff / (hull + shield), -1, 1); + result -= 1; } - } - - /** - * Standard producers and evaluators for TacticalAI - * - * These are static methods that may be used as base for TacticalAI ruleset. - */ - export class TacticalAIHelpers { - /** - * Iterator of a list of "random" arena coordinates, based on a grid - */ - static scanArena(battle: Battle, cells = 10, random = RandomGenerator.global): Iterable { - return imap(irange(cells * cells), cellpos => { - let y = Math.floor(cellpos / cells); - let x = cellpos - y * cells; - return Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells); - }); - } - - /** - * Produce a turn end. - */ - static produceEndTurn(ship: Ship, battle: Battle): TacticalProducer { - return isingle(new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); - } - - /** - * Produce all "direct hit" weapon shots. - */ - static produceDirectShots(ship: Ship, battle: Battle): TacticalProducer { - let enemies = battle.ienemies(ship, true); - let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction); - return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy))); - } - - /** - * Produce random moves inside arena cell - */ - static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer { - let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction); - return ichainit(imap(irange(iterations), iteration => { - let moves = icombine(engines, TacticalAIHelpers.scanArena(battle, cells, random)); - return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target)); - })); - } - - /** - * Produce blast weapon shots, with multiple targets. - */ - static produceInterestingBlastShots(ship: Ship, battle: Battle): TacticalProducer { - // TODO Work with groups of 3, 4 ... - let weapons = ifilter(ifilterclass(getPlayableActions(ship), TriggerAction), action => action.blast > 0); - let enemies = battle.ienemies(ship, true); - // TODO This produces duplicates (x, y) and (y, x) - let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2); - let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.blast * 2); - let result = imap(candidates, ([weapon, [e1, e2]]) => new Maneuver(ship, weapon, Target.newFromLocation((e1.arena_x + e2.arena_x) / 2, (e1.arena_y + e2.arena_y) / 2))); - return result; - } - - /** - * Produce random blast weapon shots, on a grid. - */ - static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer { - let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0); - let candidates = ifilter(icombine(weapons, TacticalAIHelpers.scanArena(battle)), ([weapon, location]) => (weapon).getEffects(ship, location).length > 0); - let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location)); - return result; - } - - /** - * Produce interesting then random blast shots - */ - static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer { - return ichain(TacticalAIHelpers.produceInterestingBlastShots(ship, battle), TacticalAIHelpers.produceRandomBlastShots(ship, battle)); - } - - /** - * Produce toggle actions at random locations. - */ - static produceToggleActions(ship: Ship, battle: Battle): TacticalProducer { - let toggles = ifilter(getPlayableActions(ship), action => action instanceof ToggleAction); - - let self_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SELF_CONFIRM, ActionTargettingMode.SELF], toggle.getTargettingMode(ship))); - let self_maneuvers = imap(self_toggles, toggle => new Maneuver(ship, toggle, Target.newFromShip(ship))); - - let distant_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SPACE, ActionTargettingMode.SURROUNDINGS], toggle.getTargettingMode(ship))); - let grid = TacticalAIHelpers.scanArena(battle); - let distant_maneuvers = imap(icombine(grid, distant_toggles), ([location, toggle]) => new Maneuver(ship, toggle, location)); - - return ichain(self_maneuvers, distant_maneuvers); - } - - /** - * Evaluate the number of turns necessary for the maneuver, between -1 and 1 - */ - static evaluateTurnCost(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let powerusage = maneuver.simulation.total_move_ap + maneuver.simulation.total_fire_ap; - if (powerusage == 0) { - return -1; - } else if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) { - return -Infinity; - } else if (powerusage > ship.getValue("power")) { - return -1; - } else { - return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity"); - } - } - - /** - * Evaluate doing nothing, between -1 and 1 - */ - static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let power_capacity = ship.getAttribute("power_capacity") || 1; - - if (maneuver.action instanceof EndTurnAction) { - return -ship.getValue("power") / power_capacity; - } else if (maneuver.action instanceof TriggerAction) { - return 0.5; - } else if (maneuver.action instanceof ToggleAction) { - return ship.actions.isToggled(maneuver.action) ? -0.2 : 0.5; - } else if (maneuver.action instanceof MoveAction) { - return -(ship.getValue("power") - maneuver.getPowerUsage()) / power_capacity; - } else { - return 0; - } - } - - /** - * Evaluate the effect on health for a group of ships - */ - static evaluateHealthEffect(maneuver: Maneuver, ships: Ship[]): number { - if (ships.length) { - let diffs = ships.map(ship => getProportionalHealth(maneuver, ship)); - let deaths = sum(diffs.map(i => i == -1 ? 1 : 0)); - return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length; - } else { - return 0; - } - } - - /** - * Evaluate the effect on health to the enemy, between -1 and 1 - */ - static evaluateEnemyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let enemies = imaterialize(battle.ienemies(ship, true)); - return -TacticalAIHelpers.evaluateHealthEffect(maneuver, enemies); - } - - /** - * Evaluate the effect on health to allied ships, between -1 and 1 - */ - static evaluateAllyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let allies = imaterialize(battle.iallies(ship, true)); - return TacticalAIHelpers.evaluateHealthEffect(maneuver, allies); - } - - /** - * Evaluate the clustering of ships, between -1 and 1 - */ - static evaluateClustering(ship: Ship, battle: Battle, maneuver: Maneuver): number { - // TODO Take into account blast radius of in-play weapons - let move_location = maneuver.getFinalLocation(); - let distances = imaterialize(imap(ifilter(battle.iships(), iship => iship != ship), iship => Target.newFromShip(iship).getDistanceTo(move_location))); - if (distances.length == 0) { - return 0; - } else { - let factor = max([battle.width, battle.height]) * 0.01; - let result = -clamp(sum(distances.map(distance => factor / distance)), 0, 1); - return result; - } - } - - /** - * Evaluate the global positioning of a ship on the arena, between -1 and 1 - */ - static evaluatePosition(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let pos = maneuver.getFinalLocation(); - let distance = min([pos.x, pos.y, battle.width - pos.x, battle.height - pos.y]); - let factor = min([battle.width / 2, battle.height / 2]); - return -1 + 2 * distance / factor; - } - - /** - * Evaluate the cost of overheating an equipment - */ - static evaluateOverheat(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let cooldown = ship.actions.getCooldown(maneuver.action); - if (cooldown.willOverheat()) { - return -Math.min(1, 0.4 * cooldown.cooling); - } else { - return 0; - } - } - - /** - * Evaluate the gain or loss of active effects - */ - static evaluateActiveEffects(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let result = 0; - maneuver.effects.forEach(effect => { - if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) { - let target = battle.getShip(effect.ship_id); - let enemy = target && !target.isPlayedBy(ship.getPlayer()); - let beneficial = effect.effect.isBeneficial(); - if (effect instanceof ShipEffectRemovedDiff) { - beneficial = !beneficial; - } - // TODO Evaluate the "power" of the effect - if ((beneficial && !enemy) || (!beneficial && enemy)) { - result += 1; - } else { - result -= 1; - } - } - }); - return clamp(result / battle.ships.count(), -1, 1); - } - } -} \ No newline at end of file + } + }); + return clamp(result / battle.ships.count(), -1, 1); + } +} diff --git a/src/core/diffs/BaseBattleDiff.ts b/src/core/diffs/BaseBattleDiff.ts index 056d9c6..1302390 100644 --- a/src/core/diffs/BaseBattleDiff.ts +++ b/src/core/diffs/BaseBattleDiff.ts @@ -1,55 +1,56 @@ -/// +import { Diff } from "../../common/DiffLog"; +import { RObjectId } from "../../common/RObject"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; -module TK.SpaceTac { - /** - * Base class for battle diffs - * - * Events are the proper way to modify the battle state - */ - export class BaseBattleDiff extends Diff { - } - - /** - * Base class for battle diffs related to a ship - */ - export class BaseBattleShipDiff extends BaseBattleDiff { - ship_id: RObjectId - - constructor(ship: Ship | RObjectId) { - super(); - - this.ship_id = (ship instanceof Ship) ? ship.id : ship; - } - - apply(battle: Battle): void { - let ship = battle.getShip(this.ship_id); - if (ship) { - this.applyOnShip(ship, battle); - } else { - console.error("Diff apply failed - Ship not found", this); - } - } - - /** - * Apply the diff on the ship - */ - protected applyOnShip(ship: Ship, battle: Battle): void { - } - - revert(battle: Battle): void { - let ship = battle.getShip(this.ship_id); - if (ship) { - this.revertOnShip(ship, battle); - } else { - console.error("Diff revert failed - Ship not found", this); - } - } - - /** - * Revert the diff on the ship - */ - protected revertOnShip(ship: Ship, battle: Battle): void { - this.getReverse().apply(battle); - } - } +/** + * Base class for battle diffs + * + * Events are the proper way to modify the battle state + */ +export class BaseBattleDiff extends Diff { +} + +/** + * Base class for battle diffs related to a ship + */ +export class BaseBattleShipDiff extends BaseBattleDiff { + ship_id: RObjectId + + constructor(ship: Ship | RObjectId) { + super(); + + this.ship_id = (ship instanceof Ship) ? ship.id : ship; + } + + apply(battle: Battle): void { + let ship = battle.getShip(this.ship_id); + if (ship) { + this.applyOnShip(ship, battle); + } else { + console.error("Diff apply failed - Ship not found", this); + } + } + + /** + * Apply the diff on the ship + */ + protected applyOnShip(ship: Ship, battle: Battle): void { + } + + revert(battle: Battle): void { + let ship = battle.getShip(this.ship_id); + if (ship) { + this.revertOnShip(ship, battle); + } else { + console.error("Diff revert failed - Ship not found", this); + } + } + + /** + * Revert the diff on the ship + */ + protected revertOnShip(ship: Ship, battle: Battle): void { + this.getReverse().apply(battle); + } } diff --git a/src/core/diffs/DroneDeployedDiff.spec.ts b/src/core/diffs/DroneDeployedDiff.spec.ts index 02a0546..1fe5722 100644 --- a/src/core/diffs/DroneDeployedDiff.spec.ts +++ b/src/core/diffs/DroneDeployedDiff.spec.ts @@ -1,32 +1,30 @@ -module TK.SpaceTac.Specs { - testing("DroneDeployedDiff", test => { - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let drone1 = new Drone(battle.play_order[0]); - let drone2 = new Drone(battle.play_order[0], "test"); +testing("DroneDeployedDiff", test => { + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let drone1 = new Drone(battle.play_order[0]); + let drone2 = new Drone(battle.play_order[0], "test"); - TestTools.diffChain(check, battle, [ - new DroneDeployedDiff(drone1), - new DroneDeployedDiff(drone2), - new DroneRecalledDiff(drone1), - new DroneRecalledDiff(drone2), - ], [ - check => { - check.equals(battle.drones.count(), 0, "drone count"); - }, - check => { - check.equals(battle.drones.count(), 1, "drone count"); - }, - check => { - check.equals(battle.drones.count(), 2, "drone count"); - }, - check => { - check.equals(battle.drones.count(), 1, "drone count"); - }, - check => { - check.equals(battle.drones.count(), 0, "drone count"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new DroneDeployedDiff(drone1), + new DroneDeployedDiff(drone2), + new DroneRecalledDiff(drone1), + new DroneRecalledDiff(drone2), + ], [ + check => { + check.equals(battle.drones.count(), 0, "drone count"); + }, + check => { + check.equals(battle.drones.count(), 1, "drone count"); + }, + check => { + check.equals(battle.drones.count(), 2, "drone count"); + }, + check => { + check.equals(battle.drones.count(), 1, "drone count"); + }, + check => { + check.equals(battle.drones.count(), 0, "drone count"); + }, + ]); + }); +}); diff --git a/src/core/diffs/DroneDeployedDiff.ts b/src/core/diffs/DroneDeployedDiff.ts index 23427a3..7da70c9 100644 --- a/src/core/diffs/DroneDeployedDiff.ts +++ b/src/core/diffs/DroneDeployedDiff.ts @@ -1,47 +1,48 @@ -/// +import { Battle } from "../Battle"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A drone is deployed by a ship - */ - export class DroneDeployedDiff extends BaseBattleShipDiff { - // Drone object - drone: Drone +/** + * A drone is deployed by a ship + */ +export class DroneDeployedDiff extends BaseBattleShipDiff { + // Drone object + drone: Drone - constructor(drone: Drone) { - super(drone.owner); + constructor(drone: Drone) { + super(drone.owner); - this.drone = drone; - } + this.drone = drone; + } - protected applyOnShip(ship: Ship, battle: Battle): void { - battle.addDrone(this.drone); - } + protected applyOnShip(ship: Ship, battle: Battle): void { + battle.addDrone(this.drone); + } - protected getReverse(): BaseBattleDiff { - return new DroneRecalledDiff(this.drone); - } - } - - /** - * A drone is recalled - */ - export class DroneRecalledDiff extends BaseBattleShipDiff { - // Drone object - drone: Drone - - constructor(drone: Drone) { - super(drone.owner); - - this.drone = drone; - } - - protected applyOnShip(ship: Ship, battle: Battle): void { - battle.removeDrone(this.drone); - } - - protected getReverse(): BaseBattleDiff { - return new DroneDeployedDiff(this.drone); - } - } + protected getReverse(): BaseBattleDiff { + return new DroneRecalledDiff(this.drone); + } +} + +/** + * A drone is recalled + */ +export class DroneRecalledDiff extends BaseBattleShipDiff { + // Drone object + drone: Drone + + constructor(drone: Drone) { + super(drone.owner); + + this.drone = drone; + } + + protected applyOnShip(ship: Ship, battle: Battle): void { + battle.removeDrone(this.drone); + } + + protected getReverse(): BaseBattleDiff { + return new DroneDeployedDiff(this.drone); + } } diff --git a/src/core/diffs/EndBattleDiff.spec.ts b/src/core/diffs/EndBattleDiff.spec.ts index 0188543..09dd5b1 100644 --- a/src/core/diffs/EndBattleDiff.spec.ts +++ b/src/core/diffs/EndBattleDiff.spec.ts @@ -1,25 +1,23 @@ -module TK.SpaceTac.Specs { - testing("EndBattleDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); +testing("EndBattleDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[1].addShip(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); - battle.start(); + battle.start(); - TestTools.diffChain(check, battle, [ - new EndBattleDiff(battle.fleets[1], 4) - ], [ - check => { - check.equals(battle.ended, false, "battle is ongoing"); - check.equals(battle.outcome, null, "battle has no outcome"); - }, - check => { - check.equals(battle.ended, true, "battle is ended"); - check.same(nn(battle.outcome).winner, battle.fleets[1].id, "battle has an outcome"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new EndBattleDiff(battle.fleets[1], 4) + ], [ + check => { + check.equals(battle.ended, false, "battle is ongoing"); + check.equals(battle.outcome, null, "battle has no outcome"); + }, + check => { + check.equals(battle.ended, true, "battle is ended"); + check.same(nn(battle.outcome).winner, battle.fleets[1].id, "battle has an outcome"); + }, + ]); + }); +}); diff --git a/src/core/diffs/EndBattleDiff.ts b/src/core/diffs/EndBattleDiff.ts index 9a9540e..4b6d036 100644 --- a/src/core/diffs/EndBattleDiff.ts +++ b/src/core/diffs/EndBattleDiff.ts @@ -1,31 +1,32 @@ -/// +import { Battle } from "../Battle"; +import { BattleOutcome } from "../BattleOutcome"; +import { Fleet } from "../Fleet"; +import { BaseBattleDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A battle ends - * - * This should be the last diff of a battle log - */ - export class EndBattleDiff extends BaseBattleDiff { - // Outcome of the battle - outcome: BattleOutcome +/** + * A battle ends + * + * This should be the last diff of a battle log + */ +export class EndBattleDiff extends BaseBattleDiff { + // Outcome of the battle + outcome: BattleOutcome - // Number of battle cycles - cycles: number + // Number of battle cycles + cycles: number - constructor(winner: Fleet | null, cycles: number) { - super(); + constructor(winner: Fleet | null, cycles: number) { + super(); - this.outcome = new BattleOutcome(winner); - this.cycles = cycles; - } + this.outcome = new BattleOutcome(winner); + this.cycles = cycles; + } - apply(battle: Battle): void { - battle.outcome = this.outcome; - } + apply(battle: Battle): void { + battle.outcome = this.outcome; + } - revert(battle: Battle): void { - battle.outcome = null; - } - } + revert(battle: Battle): void { + battle.outcome = null; + } } diff --git a/src/core/diffs/ProjectileFiredDiff.ts b/src/core/diffs/ProjectileFiredDiff.ts index e2af613..cffb9f6 100644 --- a/src/core/diffs/ProjectileFiredDiff.ts +++ b/src/core/diffs/ProjectileFiredDiff.ts @@ -1,20 +1,22 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { TriggerAction } from "../actions/TriggerAction"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A projectile is fired - * - * This does not do anything, and is just there for animations - */ - export class ProjectileFiredDiff extends BaseBattleShipDiff { - action: RObjectId - target: Target +/** + * A projectile is fired + * + * This does not do anything, and is just there for animations + */ +export class ProjectileFiredDiff extends BaseBattleShipDiff { + action: RObjectId + target: Target - constructor(ship: Ship, action: TriggerAction, target: Target) { - super(ship); + constructor(ship: Ship, action: TriggerAction, target: Target) { + super(ship); - this.action = action.id; - this.target = target; - } - } + this.action = action.id; + this.target = target; + } } diff --git a/src/core/diffs/ShipActionEndedDiff.ts b/src/core/diffs/ShipActionEndedDiff.ts index 7da926f..e965329 100644 --- a/src/core/diffs/ShipActionEndedDiff.ts +++ b/src/core/diffs/ShipActionEndedDiff.ts @@ -1,23 +1,25 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { BaseAction } from "../actions/BaseAction"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship action is fully ended - * - * This does not do anything, it is just used to mark the effective end of the action diffs (battle checks included) - */ - export class ShipActionEndedDiff extends BaseBattleShipDiff { - // Action applied - action: RObjectId +/** + * A ship action is fully ended + * + * This does not do anything, it is just used to mark the effective end of the action diffs (battle checks included) + */ +export class ShipActionEndedDiff extends BaseBattleShipDiff { + // Action applied + action: RObjectId - // Target for the action - target: Target + // Target for the action + target: Target - constructor(ship: Ship, action: BaseAction, target: Target) { - super(ship); + constructor(ship: Ship, action: BaseAction, target: Target) { + super(ship); - this.action = action.id; - this.target = target; - } - } + this.action = action.id; + this.target = target; + } } diff --git a/src/core/diffs/ShipActionToggleDiff.spec.ts b/src/core/diffs/ShipActionToggleDiff.spec.ts index dbbc135..0c53065 100644 --- a/src/core/diffs/ShipActionToggleDiff.spec.ts +++ b/src/core/diffs/ShipActionToggleDiff.spec.ts @@ -1,27 +1,25 @@ -module TK.SpaceTac.Specs { - testing("ShipActionToggleDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); +testing("ShipActionToggleDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); - let generator = TestTools.setShipModel(ship, 100, 0, 10); - let action = new ToggleAction("testtoggle", { power: 2 }); - ship.actions.addCustom(action); + let generator = TestTools.setShipModel(ship, 100, 0, 10); + let action = new ToggleAction("testtoggle", { power: 2 }); + ship.actions.addCustom(action); - TestTools.diffChain(check, battle, [ - new ShipActionToggleDiff(ship, action, true), - new ShipActionToggleDiff(ship, action, false), - ], [ - check => { - check.equals(ship.actions.isToggled(action), false, "not activated"); - }, - check => { - check.equals(ship.actions.isToggled(action), true, "activated"); - }, - check => { - check.equals(ship.actions.isToggled(action), false, "not activated"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipActionToggleDiff(ship, action, true), + new ShipActionToggleDiff(ship, action, false), + ], [ + check => { + check.equals(ship.actions.isToggled(action), false, "not activated"); + }, + check => { + check.equals(ship.actions.isToggled(action), true, "activated"); + }, + check => { + check.equals(ship.actions.isToggled(action), false, "not activated"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipActionToggleDiff.ts b/src/core/diffs/ShipActionToggleDiff.ts index c3d26d9..0cf08d0 100644 --- a/src/core/diffs/ShipActionToggleDiff.ts +++ b/src/core/diffs/ShipActionToggleDiff.ts @@ -1,39 +1,43 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { BaseAction } from "../actions/BaseAction"; +import { ToggleAction } from "../actions/ToggleAction"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship activated or deactivated a toggle action - */ - export class ShipActionToggleDiff extends BaseBattleShipDiff { - // Pointer to the action - action: RObjectId +/** + * A ship activated or deactivated a toggle action + */ +export class ShipActionToggleDiff extends BaseBattleShipDiff { + // Pointer to the action + action: RObjectId - // true for activation, false for deactivation - activated: boolean + // true for activation, false for deactivation + activated: boolean - constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, activated: boolean) { - super(ship); + constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, activated: boolean) { + super(ship); - this.action = (action instanceof BaseAction) ? action.id : action; - this.activated = activated; - } + this.action = (action instanceof BaseAction) ? action.id : action; + this.activated = activated; + } - applyOnShip(ship: Ship, battle: Battle): void { - let action = ship.actions.getById(this.action); - if (action && action instanceof ToggleAction) { - let activated = ship.actions.isToggled(action); - if (activated == this.activated) { - console.warn("Diff not applied - action already in good state", this, action); - } else { - ship.actions.toggle(action, this.activated); - } - } else { - console.error("Diff not applied - action not found on ship", this, ship); - } - } - - getReverse(): BaseBattleDiff { - return new ShipActionToggleDiff(this.ship_id, this.action, !this.activated); - } + applyOnShip(ship: Ship, battle: Battle): void { + let action = ship.actions.getById(this.action); + if (action && action instanceof ToggleAction) { + let activated = ship.actions.isToggled(action); + if (activated == this.activated) { + console.warn("Diff not applied - action already in good state", this, action); + } else { + ship.actions.toggle(action, this.activated); + } + } else { + console.error("Diff not applied - action not found on ship", this, ship); } + } + + getReverse(): BaseBattleDiff { + return new ShipActionToggleDiff(this.ship_id, this.action, !this.activated); + } } + diff --git a/src/core/diffs/ShipActionUsedDiff.spec.ts b/src/core/diffs/ShipActionUsedDiff.spec.ts index 5271582..a7e32aa 100644 --- a/src/core/diffs/ShipActionUsedDiff.spec.ts +++ b/src/core/diffs/ShipActionUsedDiff.spec.ts @@ -1,28 +1,26 @@ -module TK.SpaceTac.Specs { - testing("ShipActionUsedDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); +testing("ShipActionUsedDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); - let generator = TestTools.setShipModel(ship, 100, 0, 10); - let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20); - weapon.configureCooldown(2, 1); - let cooldown = ship.actions.getCooldown(weapon); + let generator = TestTools.setShipModel(ship, 100, 0, 10); + let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20); + weapon.configureCooldown(2, 1); + let cooldown = ship.actions.getCooldown(weapon); - TestTools.diffChain(check, battle, [ - new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), - new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), - ], [ - check => { - check.equals(cooldown.getRemainingUses(), 2, "cooldown"); - }, - check => { - check.equals(cooldown.getRemainingUses(), 1, "cooldown"); - }, - check => { - check.equals(cooldown.getRemainingUses(), 0, "cooldown"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), + new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), + ], [ + check => { + check.equals(cooldown.getRemainingUses(), 2, "cooldown"); + }, + check => { + check.equals(cooldown.getRemainingUses(), 1, "cooldown"); + }, + check => { + check.equals(cooldown.getRemainingUses(), 0, "cooldown"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipActionUsedDiff.ts b/src/core/diffs/ShipActionUsedDiff.ts index 41a73b3..7545509 100644 --- a/src/core/diffs/ShipActionUsedDiff.ts +++ b/src/core/diffs/ShipActionUsedDiff.ts @@ -1,52 +1,55 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { BaseAction } from "../actions/BaseAction"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { Target } from "../Target"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship uses an action - * - * This will change: - * - The cooldown on the action and/or equipment - * - The wearing down of the equipment - */ - export class ShipActionUsedDiff extends BaseBattleShipDiff { - // Action applied - action: RObjectId +/** + * A ship uses an action + * + * This will change: + * - The cooldown on the action and/or equipment + * - The wearing down of the equipment + */ +export class ShipActionUsedDiff extends BaseBattleShipDiff { + // Action applied + action: RObjectId - // Target for the action - target: Target + // Target for the action + target: Target - constructor(ship: Ship, action: BaseAction, target: Target) { - super(ship); + constructor(ship: Ship, action: BaseAction, target: Target) { + super(ship); - this.action = action.id; - this.target = target; - } + this.action = action.id; + this.target = target; + } - protected applyOnShip(ship: Ship, battle: Battle): void { - let action = ship.actions.getById(this.action); + protected applyOnShip(ship: Ship, battle: Battle): void { + let action = ship.actions.getById(this.action); - if (!action) { - console.error("Action failed - not found on ship", this, ship); - return; - } - - if (ship.actions.isUsable(action)) { - ship.actions.storeUsage(action, 1); - } else { - console.error("Action apply failed - in cooldown", this, ship); - return; - } - } - - protected revertOnShip(ship: Ship, battle: Battle): void { - let action = ship.actions.getById(this.action); - - if (!action) { - console.error("Action revert failed - not found on ship", this, ship); - return; - } - - ship.actions.storeUsage(action, -1); - } + if (!action) { + console.error("Action failed - not found on ship", this, ship); + return; } + + if (ship.actions.isUsable(action)) { + ship.actions.storeUsage(action, 1); + } else { + console.error("Action apply failed - in cooldown", this, ship); + return; + } + } + + protected revertOnShip(ship: Ship, battle: Battle): void { + let action = ship.actions.getById(this.action); + + if (!action) { + console.error("Action revert failed - not found on ship", this, ship); + return; + } + + ship.actions.storeUsage(action, -1); + } } diff --git a/src/core/diffs/ShipAttributeDiff.spec.ts b/src/core/diffs/ShipAttributeDiff.spec.ts index bf8d57c..21f489c 100644 --- a/src/core/diffs/ShipAttributeDiff.spec.ts +++ b/src/core/diffs/ShipAttributeDiff.spec.ts @@ -1,51 +1,49 @@ -module TK.SpaceTac.Specs { - testing("ShipAttributeDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); +testing("ShipAttributeDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); - TestTools.diffChain(check, battle, [ - new ShipAttributeDiff(ship, "power_capacity", { cumulative: 5 }, {}), - new ShipAttributeDiff(ship, "evasion", { cumulative: 8 }, {}), - new ShipAttributeDiff(ship, "power_capacity", { cumulative: 2 }, {}), - new ShipAttributeDiff(ship, "power_capacity", { cumulative: 4 }, { cumulative: 5 }), - new ShipAttributeDiff(ship, "evasion", { multiplier: 50 }, {}), - new ShipAttributeDiff(ship, "evasion", { limit: 2 }, {}), - new ShipAttributeDiff(ship, "evasion", {}, { multiplier: 50, limit: 2 }), - ], [ - check => { - check.equals(ship.getAttribute("power_capacity"), 0, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 0, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 5, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 0, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 5, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 8, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 7, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 8, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 8, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 12, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 2, "evasion value"); - }, - check => { - check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); - check.equals(ship.getAttribute("evasion"), 8, "evasion value"); - }, - ]) - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipAttributeDiff(ship, "power_capacity", { cumulative: 5 }, {}), + new ShipAttributeDiff(ship, "evasion", { cumulative: 8 }, {}), + new ShipAttributeDiff(ship, "power_capacity", { cumulative: 2 }, {}), + new ShipAttributeDiff(ship, "power_capacity", { cumulative: 4 }, { cumulative: 5 }), + new ShipAttributeDiff(ship, "evasion", { multiplier: 50 }, {}), + new ShipAttributeDiff(ship, "evasion", { limit: 2 }, {}), + new ShipAttributeDiff(ship, "evasion", {}, { multiplier: 50, limit: 2 }), + ], [ + check => { + check.equals(ship.getAttribute("power_capacity"), 0, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 0, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 5, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 0, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 5, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 8, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 7, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 8, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 8, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 12, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 2, "evasion value"); + }, + check => { + check.equals(ship.getAttribute("power_capacity"), 6, "power capacity value"); + check.equals(ship.getAttribute("evasion"), 8, "evasion value"); + }, + ]) + }); +}); diff --git a/src/core/diffs/ShipAttributeDiff.ts b/src/core/diffs/ShipAttributeDiff.ts index fdf19c7..c7f4d6d 100644 --- a/src/core/diffs/ShipAttributeDiff.ts +++ b/src/core/diffs/ShipAttributeDiff.ts @@ -1,41 +1,43 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { ShipAttributes } from "../ShipValue"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - type ShipAttributeModifier = { - cumulative?: number, - multiplier?: number, - limit?: number - } +type ShipAttributeModifier = { + cumulative?: number, + multiplier?: number, + limit?: number +} - /** - * A ship attribute modifier changed - */ - export class ShipAttributeDiff extends BaseBattleShipDiff { - // Attribute that changes - code: keyof ShipAttributes +/** + * A ship attribute modifier changed + */ +export class ShipAttributeDiff extends BaseBattleShipDiff { + // Attribute that changes + code: keyof ShipAttributes - // Modifiers added - added: ShipAttributeModifier + // Modifiers added + added: ShipAttributeModifier - // Modifiers removed - removed: ShipAttributeModifier + // Modifiers removed + removed: ShipAttributeModifier - constructor(ship: Ship | RObjectId, code: keyof ShipAttributes, added: ShipAttributeModifier, removed: ShipAttributeModifier) { - super(ship); + constructor(ship: Ship | RObjectId, code: keyof ShipAttributes, added: ShipAttributeModifier, removed: ShipAttributeModifier) { + super(ship); - this.code = code; - this.added = added; - this.removed = removed; - } + this.code = code; + this.added = added; + this.removed = removed; + } - getReverse(): BaseBattleDiff { - return new ShipAttributeDiff(this.ship_id, this.code, this.removed, this.added); - } + getReverse(): BaseBattleDiff { + return new ShipAttributeDiff(this.ship_id, this.code, this.removed, this.added); + } - applyOnShip(ship: Ship, battle: Battle): void { - let attribute = ship.attributes[this.code]; - attribute.addModifier(this.added.cumulative, this.added.multiplier, this.added.limit); - attribute.removeModifier(this.removed.cumulative, this.removed.multiplier, this.removed.limit); - } - } -} \ No newline at end of file + applyOnShip(ship: Ship, battle: Battle): void { + let attribute = ship.attributes[this.code]; + attribute.addModifier(this.added.cumulative, this.added.multiplier, this.added.limit); + attribute.removeModifier(this.removed.cumulative, this.removed.multiplier, this.removed.limit); + } +} diff --git a/src/core/diffs/ShipChangeDiff.spec.ts b/src/core/diffs/ShipChangeDiff.spec.ts index 19f36fd..3171cb4 100644 --- a/src/core/diffs/ShipChangeDiff.spec.ts +++ b/src/core/diffs/ShipChangeDiff.spec.ts @@ -1,36 +1,34 @@ -module TK.SpaceTac.Specs { - testing("ShipChangeDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let attacker1 = battle.fleets[0].addShip(); - let attacker2 = battle.fleets[0].addShip(); - let defender1 = battle.fleets[1].addShip(); - battle.play_order = [defender1, attacker2, attacker1]; - battle.play_index = 0; - battle.cycle = 1; +testing("ShipChangeDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let attacker1 = battle.fleets[0].addShip(); + let attacker2 = battle.fleets[0].addShip(); + let defender1 = battle.fleets[1].addShip(); + battle.play_order = [defender1, attacker2, attacker1]; + battle.play_index = 0; + battle.cycle = 1; - TestTools.diffChain(check, battle, [ - new ShipChangeDiff(battle.play_order[0], battle.play_order[1]), - new ShipChangeDiff(battle.play_order[1], battle.play_order[2]), - new ShipChangeDiff(battle.play_order[2], battle.play_order[0], 1), - ], [ - check => { - check.same(battle.playing_ship, defender1, "first ship playing"); - check.equals(battle.cycle, 1, "first cycle"); - }, - check => { - check.same(battle.playing_ship, attacker2, "second ship playing"); - check.equals(battle.cycle, 1, "first cycle"); - }, - check => { - check.same(battle.playing_ship, attacker1, "third ship playing"); - check.equals(battle.cycle, 1, "first cycle"); - }, - check => { - check.same(battle.playing_ship, defender1, "first ship playing again"); - check.equals(battle.cycle, 2, "second cycle"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipChangeDiff(battle.play_order[0], battle.play_order[1]), + new ShipChangeDiff(battle.play_order[1], battle.play_order[2]), + new ShipChangeDiff(battle.play_order[2], battle.play_order[0], 1), + ], [ + check => { + check.same(battle.playing_ship, defender1, "first ship playing"); + check.equals(battle.cycle, 1, "first cycle"); + }, + check => { + check.same(battle.playing_ship, attacker2, "second ship playing"); + check.equals(battle.cycle, 1, "first cycle"); + }, + check => { + check.same(battle.playing_ship, attacker1, "third ship playing"); + check.equals(battle.cycle, 1, "first cycle"); + }, + check => { + check.same(battle.playing_ship, defender1, "first ship playing again"); + check.equals(battle.cycle, 2, "second cycle"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipChangeDiff.ts b/src/core/diffs/ShipChangeDiff.ts index 1a24b60..1808505 100644 --- a/src/core/diffs/ShipChangeDiff.ts +++ b/src/core/diffs/ShipChangeDiff.ts @@ -1,39 +1,40 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * Current playing ship changes - */ - export class ShipChangeDiff extends BaseBattleShipDiff { - // ID of the new playing ship - new_ship: RObjectId +/** + * Current playing ship changes + */ +export class ShipChangeDiff extends BaseBattleShipDiff { + // ID of the new playing ship + new_ship: RObjectId - // Diff in the cycle count - cycle_diff: number + // Diff in the cycle count + cycle_diff: number - constructor(ship: Ship | RObjectId, new_ship: Ship | RObjectId, cycle_diff = 0) { - super(ship); + constructor(ship: Ship | RObjectId, new_ship: Ship | RObjectId, cycle_diff = 0) { + super(ship); - this.new_ship = (new_ship instanceof Ship) ? new_ship.id : new_ship; - this.cycle_diff = cycle_diff; - } + this.new_ship = (new_ship instanceof Ship) ? new_ship.id : new_ship; + this.cycle_diff = cycle_diff; + } - applyOnShip(ship: Ship, battle: Battle) { - if (ship.is(battle.playing_ship)) { - let new_ship = battle.getShip(this.new_ship); - if (new_ship) { - battle.setPlayingShip(new_ship); - battle.cycle += this.cycle_diff; - } else { - console.error("Cannot apply diff - new ship not found", this); - } - } else { - console.error("Cannot apply diff - ship is not playing", this); - } - } - - getReverse(): BaseBattleDiff { - return new ShipChangeDiff(this.new_ship, this.ship_id, -this.cycle_diff); - } + applyOnShip(ship: Ship, battle: Battle) { + if (ship.is(battle.playing_ship)) { + let new_ship = battle.getShip(this.new_ship); + if (new_ship) { + battle.setPlayingShip(new_ship); + battle.cycle += this.cycle_diff; + } else { + console.error("Cannot apply diff - new ship not found", this); + } + } else { + console.error("Cannot apply diff - ship is not playing", this); } + } + + getReverse(): BaseBattleDiff { + return new ShipChangeDiff(this.new_ship, this.ship_id, -this.cycle_diff); + } } diff --git a/src/core/diffs/ShipCooldownDiff.spec.ts b/src/core/diffs/ShipCooldownDiff.spec.ts index a310ee8..54e9338 100644 --- a/src/core/diffs/ShipCooldownDiff.spec.ts +++ b/src/core/diffs/ShipCooldownDiff.spec.ts @@ -1,30 +1,28 @@ -module TK.SpaceTac.Specs { - testing("ShipCooldownDiff", test => { - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - let weapon = TestTools.addWeapon(ship); - weapon.configureCooldown(1, 3); - let cooldown = ship.actions.getCooldown(weapon); - cooldown.use(); +testing("ShipCooldownDiff", test => { + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; + let weapon = TestTools.addWeapon(ship); + weapon.configureCooldown(1, 3); + let cooldown = ship.actions.getCooldown(weapon); + cooldown.use(); - TestTools.diffChain(check, battle, [ - new ShipCooldownDiff(ship, weapon, 1), - new ShipCooldownDiff(ship, weapon, 2), - ], [ - check => { - check.equals(cooldown.heat, 3, "heat"); - check.equals(cooldown.uses, 1, "uses"); - }, - check => { - check.equals(cooldown.heat, 2, "heat"); - check.equals(cooldown.uses, 1, "uses"); - }, - check => { - check.equals(cooldown.heat, 0, "heat"); - check.equals(cooldown.uses, 0, "uses"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipCooldownDiff(ship, weapon, 1), + new ShipCooldownDiff(ship, weapon, 2), + ], [ + check => { + check.equals(cooldown.heat, 3, "heat"); + check.equals(cooldown.uses, 1, "uses"); + }, + check => { + check.equals(cooldown.heat, 2, "heat"); + check.equals(cooldown.uses, 1, "uses"); + }, + check => { + check.equals(cooldown.heat, 0, "heat"); + check.equals(cooldown.uses, 0, "uses"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipCooldownDiff.ts b/src/core/diffs/ShipCooldownDiff.ts index 62ae861..2ca77dd 100644 --- a/src/core/diffs/ShipCooldownDiff.ts +++ b/src/core/diffs/ShipCooldownDiff.ts @@ -1,47 +1,49 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { BaseAction } from "../actions/BaseAction"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship's action cools down - */ - export class ShipCooldownDiff extends BaseBattleShipDiff { - // Action to cool - action: RObjectId +/** + * A ship's action cools down + */ +export class ShipCooldownDiff extends BaseBattleShipDiff { + // Action to cool + action: RObjectId - // Quantity of heat to dissipate - heat: number + // Quantity of heat to dissipate + heat: number - constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, heat: number) { - super(ship); + constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, heat: number) { + super(ship); - this.action = (action instanceof BaseAction) ? action.id : action; - this.heat = heat; - } + this.action = (action instanceof BaseAction) ? action.id : action; + this.heat = heat; + } - applyOnShip(ship: Ship, battle: Battle) { - let action = ship.actions.getById(this.action); - if (action) { - let cooldown = ship.actions.getCooldown(action); - cooldown.heat -= this.heat; - if (cooldown.heat == 0) { - cooldown.uses = 0; - } - } else { - console.error("Cannot apply diff, action not found", this, ship.actions); - } - } - - revertOnShip(ship: Ship, battle: Battle) { - let action = ship.actions.getById(this.action); - if (action) { - let cooldown = ship.actions.getCooldown(action); - if (cooldown.heat == 0) { - cooldown.uses = cooldown.overheat; - } - cooldown.heat += this.heat; - } else { - console.error("Cannot revert diff, action not found", this, ship.actions); - } - } + applyOnShip(ship: Ship, battle: Battle) { + let action = ship.actions.getById(this.action); + if (action) { + let cooldown = ship.actions.getCooldown(action); + cooldown.heat -= this.heat; + if (cooldown.heat == 0) { + cooldown.uses = 0; + } + } else { + console.error("Cannot apply diff, action not found", this, ship.actions); } + } + + revertOnShip(ship: Ship, battle: Battle) { + let action = ship.actions.getById(this.action); + if (action) { + let cooldown = ship.actions.getCooldown(action); + if (cooldown.heat == 0) { + cooldown.uses = cooldown.overheat; + } + cooldown.heat += this.heat; + } else { + console.error("Cannot revert diff, action not found", this, ship.actions); + } + } } diff --git a/src/core/diffs/ShipDamageDiff.spec.ts b/src/core/diffs/ShipDamageDiff.spec.ts index a6a4888..66becb0 100644 --- a/src/core/diffs/ShipDamageDiff.spec.ts +++ b/src/core/diffs/ShipDamageDiff.spec.ts @@ -1,32 +1,30 @@ -module TK.SpaceTac.Specs { - testing("ShipDamageDiff", test => { - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - TestTools.setShipModel(ship, 80, 100); +testing("ShipDamageDiff", test => { + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; + TestTools.setShipModel(ship, 80, 100); - TestTools.diffChain(check, battle, [ - new ShipDamageDiff(ship, 0, 10), - new ShipDamageDiff(ship, 19, 0), - new ShipDamageDiff(ship, 30, 90), - ], [ - check => { - check.equals(ship.getValue("hull"), 80, "hull value"); - check.equals(ship.getValue("shield"), 100, "shield value"); - }, - check => { - check.equals(ship.getValue("hull"), 80, "hull value"); - check.equals(ship.getValue("shield"), 100, "shield value"); - }, - check => { - check.equals(ship.getValue("hull"), 80, "hull value"); - check.equals(ship.getValue("shield"), 100, "shield value"); - }, - check => { - check.equals(ship.getValue("hull"), 80, "hull value"); - check.equals(ship.getValue("shield"), 100, "shield value"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipDamageDiff(ship, 0, 10), + new ShipDamageDiff(ship, 19, 0), + new ShipDamageDiff(ship, 30, 90), + ], [ + check => { + check.equals(ship.getValue("hull"), 80, "hull value"); + check.equals(ship.getValue("shield"), 100, "shield value"); + }, + check => { + check.equals(ship.getValue("hull"), 80, "hull value"); + check.equals(ship.getValue("shield"), 100, "shield value"); + }, + check => { + check.equals(ship.getValue("hull"), 80, "hull value"); + check.equals(ship.getValue("shield"), 100, "shield value"); + }, + check => { + check.equals(ship.getValue("hull"), 80, "hull value"); + check.equals(ship.getValue("shield"), 100, "shield value"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipDamageDiff.ts b/src/core/diffs/ShipDamageDiff.ts index 3e7b796..10105b9 100644 --- a/src/core/diffs/ShipDamageDiff.ts +++ b/src/core/diffs/ShipDamageDiff.ts @@ -1,31 +1,30 @@ -/// +import { Ship } from "../Ship"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship takes damage (to hull or shield) - * - * This is only informative, and does not apply the damage on ship values (there are ShipValueDiff for this). - */ - export class ShipDamageDiff extends BaseBattleShipDiff { - // Damage to hull - hull: number +/** + * A ship takes damage (to hull or shield) + * + * This is only informative, and does not apply the damage on ship values (there are ShipValueDiff for this). + */ +export class ShipDamageDiff extends BaseBattleShipDiff { + // Damage to hull + hull: number - // Damage to shield - shield: number + // Damage to shield + shield: number - // Evaded damage - evaded: number + // Evaded damage + evaded: number - // Theoretical damage value - theoretical: number + // Theoretical damage value + theoretical: number - constructor(ship: Ship, hull: number, shield: number, evaded = 0, theoretical = hull + shield + evaded) { - super(ship); + constructor(ship: Ship, hull: number, shield: number, evaded = 0, theoretical = hull + shield + evaded) { + super(ship); - this.hull = hull; - this.shield = shield; - this.evaded = evaded; - this.theoretical = theoretical; - } - } + this.hull = hull; + this.shield = shield; + this.evaded = evaded; + this.theoretical = theoretical; + } } diff --git a/src/core/diffs/ShipDeathDiff.spec.ts b/src/core/diffs/ShipDeathDiff.spec.ts index fb335cd..7e92d91 100644 --- a/src/core/diffs/ShipDeathDiff.spec.ts +++ b/src/core/diffs/ShipDeathDiff.spec.ts @@ -1,32 +1,30 @@ -module TK.SpaceTac.Specs { - testing("ShipDeathDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[0].addShip(); - let ship3 = battle.fleets[1].addShip(); - battle.play_order = [ship3, ship2, ship1]; +testing("ShipDeathDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[0].addShip(); + let ship3 = battle.fleets[1].addShip(); + battle.play_order = [ship3, ship2, ship1]; - TestTools.diffChain(check, battle, [ - new ShipDeathDiff(battle, ship2) - ], [ - check => { - check.equals(ship2.alive, true, "alive"); - check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships"); - check.equals(imaterialize(battle.iships(true)), [ship1, ship2, ship3], "in alive ships"); - check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1"); - check.equals(battle.fleets[1].ships, [ship3], "fleet2"); - check.equals(battle.play_order, [ship3, ship2, ship1], "in play order"); - }, - check => { - check.equals(ship2.alive, false, "dead"); - check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships"); - check.equals(imaterialize(battle.iships(true)), [ship1, ship3], "not in alive ships anymore"); - check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1"); - check.equals(battle.fleets[1].ships, [ship3], "fleet2"); - check.equals(battle.play_order, [ship3, ship1], "removed from play order"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipDeathDiff(battle, ship2) + ], [ + check => { + check.equals(ship2.alive, true, "alive"); + check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships"); + check.equals(imaterialize(battle.iships(true)), [ship1, ship2, ship3], "in alive ships"); + check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1"); + check.equals(battle.fleets[1].ships, [ship3], "fleet2"); + check.equals(battle.play_order, [ship3, ship2, ship1], "in play order"); + }, + check => { + check.equals(ship2.alive, false, "dead"); + check.equals(imaterialize(battle.iships(false)), [ship1, ship2, ship3], "in all ships"); + check.equals(imaterialize(battle.iships(true)), [ship1, ship3], "not in alive ships anymore"); + check.equals(battle.fleets[0].ships, [ship1, ship2], "fleet1"); + check.equals(battle.fleets[1].ships, [ship3], "fleet2"); + check.equals(battle.play_order, [ship3, ship1], "removed from play order"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipDeathDiff.ts b/src/core/diffs/ShipDeathDiff.ts index 4233a25..92c7bf6 100644 --- a/src/core/diffs/ShipDeathDiff.ts +++ b/src/core/diffs/ShipDeathDiff.ts @@ -1,34 +1,34 @@ -/// +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship dies (or rather is put in emergency stasis mode) - * - * This typically happens when the ship's hull reaches 0. - * A dead ship cannot be interacted with, and will be removed from play order. - */ - export class ShipDeathDiff extends BaseBattleShipDiff { - // Index in the play order at which the ship was - play_index: number +/** + * A ship dies (or rather is put in emergency stasis mode) + * + * This typically happens when the ship's hull reaches 0. + * A dead ship cannot be interacted with, and will be removed from play order. + */ +export class ShipDeathDiff extends BaseBattleShipDiff { + // Index in the play order at which the ship was + play_index: number - constructor(battle: Battle, ship: Ship) { - super(ship); + constructor(battle: Battle, ship: Ship) { + super(ship); - this.play_index = battle.play_order.indexOf(ship); - } + this.play_index = battle.play_order.indexOf(ship); + } - protected applyOnShip(ship: Ship, battle: Battle): void { - ship.alive = false; - if (this.play_index >= 0) { - battle.removeFromPlayOrder(this.play_index); - } - } - - protected revertOnShip(ship: Ship, battle: Battle): void { - ship.alive = true; - if (this.play_index >= 0) { - battle.insertInPlayOrder(this.play_index, ship); - } - } + protected applyOnShip(ship: Ship, battle: Battle): void { + ship.alive = false; + if (this.play_index >= 0) { + battle.removeFromPlayOrder(this.play_index); } + } + + protected revertOnShip(ship: Ship, battle: Battle): void { + ship.alive = true; + if (this.play_index >= 0) { + battle.insertInPlayOrder(this.play_index, ship); + } + } } diff --git a/src/core/diffs/ShipEffectAddedDiff.spec.ts b/src/core/diffs/ShipEffectAddedDiff.spec.ts index c84c337..f590439 100644 --- a/src/core/diffs/ShipEffectAddedDiff.spec.ts +++ b/src/core/diffs/ShipEffectAddedDiff.spec.ts @@ -1,37 +1,35 @@ -module TK.SpaceTac.Specs { - testing("ShipEffectAddedDiff", test => { - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; +testing("ShipEffectAddedDiff", test => { + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; - let effect1 = new BaseEffect("e1"); - let effect2 = new BaseEffect("e2"); + let effect1 = new BaseEffect("e1"); + let effect2 = new BaseEffect("e2"); - TestTools.diffChain(check, battle, [ - new ShipEffectAddedDiff(ship, effect1), - new ShipEffectAddedDiff(ship, effect2), - new ShipEffectRemovedDiff(ship, effect1), - new ShipEffectRemovedDiff(ship, effect2), - ], [ - check => { - check.equals(ship.active_effects.count(), 0, "effect count"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.equals(ship.active_effects.get(effect1.id), effect1, "effect1 present"); - }, - check => { - check.equals(ship.active_effects.count(), 2, "effect count"); - check.equals(ship.active_effects.get(effect2.id), effect2, "effect2 present"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.equals(ship.active_effects.get(effect1.id), null, "effect1 missing"); - }, - check => { - check.equals(ship.active_effects.count(), 0, "effect count"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipEffectAddedDiff(ship, effect1), + new ShipEffectAddedDiff(ship, effect2), + new ShipEffectRemovedDiff(ship, effect1), + new ShipEffectRemovedDiff(ship, effect2), + ], [ + check => { + check.equals(ship.active_effects.count(), 0, "effect count"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.equals(ship.active_effects.get(effect1.id), effect1, "effect1 present"); + }, + check => { + check.equals(ship.active_effects.count(), 2, "effect count"); + check.equals(ship.active_effects.get(effect2.id), effect2, "effect2 present"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.equals(ship.active_effects.get(effect1.id), null, "effect1 missing"); + }, + check => { + check.equals(ship.active_effects.count(), 0, "effect count"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipEffectAddedDiff.ts b/src/core/diffs/ShipEffectAddedDiff.ts index 497d655..060689c 100644 --- a/src/core/diffs/ShipEffectAddedDiff.ts +++ b/src/core/diffs/ShipEffectAddedDiff.ts @@ -1,47 +1,51 @@ -/// +import { NAMESPACE } from "../.."; +import { RObjectId } from "../../common/RObject"; +import { duplicate } from "../../common/Tools"; +import { Battle } from "../Battle"; +import { BaseEffect } from "../effects/BaseEffect"; +import { Ship } from "../Ship"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * An effect is attached to a ship - */ - export class ShipEffectAddedDiff extends BaseBattleShipDiff { - // Effect added - effect: BaseEffect +/** + * An effect is attached to a ship + */ +export class ShipEffectAddedDiff extends BaseBattleShipDiff { + // Effect added + effect: BaseEffect - constructor(ship: Ship | RObjectId, effect: BaseEffect) { - super(ship); + constructor(ship: Ship | RObjectId, effect: BaseEffect) { + super(ship); - this.effect = duplicate(effect, TK.SpaceTac); - } + this.effect = duplicate(effect, NAMESPACE); + } - protected applyOnShip(ship: Ship, battle: Battle): void { - ship.active_effects.add(duplicate(this.effect, TK.SpaceTac)); - } + protected applyOnShip(ship: Ship, battle: Battle): void { + ship.active_effects.add(duplicate(this.effect, NAMESPACE)); + } - protected getReverse(): BaseBattleDiff { - return new ShipEffectRemovedDiff(this.ship_id, this.effect); - } - } - - /** - * An attached effect is removed from a ship - */ - export class ShipEffectRemovedDiff extends BaseBattleShipDiff { - // Effect removed - effect: BaseEffect - - constructor(ship: Ship | RObjectId, effect: BaseEffect) { - super(ship); - - this.effect = duplicate(effect, TK.SpaceTac); - } - - protected applyOnShip(ship: Ship, battle: Battle): void { - ship.active_effects.remove(this.effect); - } - - protected getReverse(): BaseBattleDiff { - return new ShipEffectAddedDiff(this.ship_id, this.effect); - } - } + protected getReverse(): BaseBattleDiff { + return new ShipEffectRemovedDiff(this.ship_id, this.effect); + } +} + +/** + * An attached effect is removed from a ship + */ +export class ShipEffectRemovedDiff extends BaseBattleShipDiff { + // Effect removed + effect: BaseEffect + + constructor(ship: Ship | RObjectId, effect: BaseEffect) { + super(ship); + + this.effect = duplicate(effect, NAMESPACE); + } + + protected applyOnShip(ship: Ship, battle: Battle): void { + ship.active_effects.remove(this.effect); + } + + protected getReverse(): BaseBattleDiff { + return new ShipEffectAddedDiff(this.ship_id, this.effect); + } } diff --git a/src/core/diffs/ShipEffectChangedDiff.spec.ts b/src/core/diffs/ShipEffectChangedDiff.spec.ts index c46d126..72464d2 100644 --- a/src/core/diffs/ShipEffectChangedDiff.spec.ts +++ b/src/core/diffs/ShipEffectChangedDiff.spec.ts @@ -1,76 +1,74 @@ -module TK.SpaceTac.Specs { - testing("ShipEffectChangedDiff", test => { - test.case("applies and reverts", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; +testing("ShipEffectChangedDiff", test => { + test.case("applies and reverts", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; - let effect1 = new BaseEffect("e1"); - let effect2 = new StickyEffect(new BaseEffect("e2"), 2); - let effect3 = new StickyEffect(new BaseEffect("e3"), 3); - ship.active_effects.add(effect1); - ship.active_effects.add(effect2); - ship.active_effects.add(effect3); + let effect1 = new BaseEffect("e1"); + let effect2 = new StickyEffect(new BaseEffect("e2"), 2); + let effect3 = new StickyEffect(new BaseEffect("e3"), 3); + ship.active_effects.add(effect1); + ship.active_effects.add(effect2); + ship.active_effects.add(effect3); - TestTools.diffChain(check, battle, [ - new ShipEffectChangedDiff(ship, effect1), - new ShipEffectChangedDiff(ship, effect2, -1), - new ShipEffectChangedDiff(ship, effect3, 1), - ], [ - check => { - check.equals(ship.active_effects.count(), 3, "effect count"); - check.equals(effect2.duration, 2, "duration effect2"); - check.equals(effect3.duration, 3, "duration effect3"); - }, - check => { - check.equals(ship.active_effects.count(), 3, "effect count"); - check.equals(effect2.duration, 2, "duration effect2"); - check.equals(effect3.duration, 3, "duration effect3"); - }, - check => { - check.equals(ship.active_effects.count(), 3, "effect count"); - check.equals(effect2.duration, 1, "duration effect2"); - check.equals(effect3.duration, 3, "duration effect3"); - }, - check => { - check.equals(ship.active_effects.count(), 3, "effect count"); - check.equals(effect2.duration, 1, "duration effect2"); - check.equals(effect3.duration, 4, "duration effect3"); - }, - ]); - }); + TestTools.diffChain(check, battle, [ + new ShipEffectChangedDiff(ship, effect1), + new ShipEffectChangedDiff(ship, effect2, -1), + new ShipEffectChangedDiff(ship, effect3, 1), + ], [ + check => { + check.equals(ship.active_effects.count(), 3, "effect count"); + check.equals(effect2.duration, 2, "duration effect2"); + check.equals(effect3.duration, 3, "duration effect3"); + }, + check => { + check.equals(ship.active_effects.count(), 3, "effect count"); + check.equals(effect2.duration, 2, "duration effect2"); + check.equals(effect3.duration, 3, "duration effect3"); + }, + check => { + check.equals(ship.active_effects.count(), 3, "effect count"); + check.equals(effect2.duration, 1, "duration effect2"); + check.equals(effect3.duration, 3, "duration effect3"); + }, + check => { + check.equals(ship.active_effects.count(), 3, "effect count"); + check.equals(effect2.duration, 1, "duration effect2"); + check.equals(effect3.duration, 4, "duration effect3"); + }, + ]); + }); - test.case("leaves original effect untouched", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; + test.case("leaves original effect untouched", check => { + let battle = TestTools.createBattle(); + let ship = battle.play_order[0]; - let effect = new StickyEffect(new BaseEffect("effect"), 2); - let effect_at_removal = copy(effect); - effect_at_removal.duration = 1; + let effect = new StickyEffect(new BaseEffect("effect"), 2); + let effect_at_removal = copy(effect); + effect_at_removal.duration = 1; - TestTools.diffChain(check, battle, [ - new ShipEffectAddedDiff(ship, effect), - new ShipEffectChangedDiff(ship, effect, -1), - new ShipEffectRemovedDiff(ship, effect_at_removal), - ], [ - check => { - check.equals(ship.active_effects.count(), 0, "effect count"); - check.equals(effect.duration, 2, "original duration"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.equals(effect.duration, 2, "original duration"); - check.equals((nn(ship.active_effects.get(effect.id))).duration, 2, "active duration"); - }, - check => { - check.equals(ship.active_effects.count(), 1, "effect count"); - check.equals(effect.duration, 2, "original duration"); - check.equals((nn(ship.active_effects.get(effect.id))).duration, 1, "active duration"); - }, - check => { - check.equals(ship.active_effects.count(), 0, "effect count"); - check.equals(effect.duration, 2, "original duration"); - }, - ]); - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipEffectAddedDiff(ship, effect), + new ShipEffectChangedDiff(ship, effect, -1), + new ShipEffectRemovedDiff(ship, effect_at_removal), + ], [ + check => { + check.equals(ship.active_effects.count(), 0, "effect count"); + check.equals(effect.duration, 2, "original duration"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.equals(effect.duration, 2, "original duration"); + check.equals((nn(ship.active_effects.get(effect.id))).duration, 2, "active duration"); + }, + check => { + check.equals(ship.active_effects.count(), 1, "effect count"); + check.equals(effect.duration, 2, "original duration"); + check.equals((nn(ship.active_effects.get(effect.id))).duration, 1, "active duration"); + }, + check => { + check.equals(ship.active_effects.count(), 0, "effect count"); + check.equals(effect.duration, 2, "original duration"); + }, + ]); + }); +}); diff --git a/src/core/diffs/ShipEffectChangedDiff.ts b/src/core/diffs/ShipEffectChangedDiff.ts index e9d8d39..438f3b1 100644 --- a/src/core/diffs/ShipEffectChangedDiff.ts +++ b/src/core/diffs/ShipEffectChangedDiff.ts @@ -1,40 +1,43 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { Battle } from "../Battle"; +import { BaseEffect } from "../effects/BaseEffect"; +import { StickyEffect } from "../effects/StickyEffect"; +import { Ship } from "../Ship"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * An effect attached to a ship changed - */ - export class ShipEffectChangedDiff extends BaseBattleShipDiff { - // Effect modified - effect: RObjectId +/** + * An effect attached to a ship changed + */ +export class ShipEffectChangedDiff extends BaseBattleShipDiff { + // Effect modified + effect: RObjectId - // Duration diff - duration: number + // Duration diff + duration: number - constructor(ship: Ship | RObjectId, effect: BaseEffect | RObjectId, duration = 0) { - super(ship); + constructor(ship: Ship | RObjectId, effect: BaseEffect | RObjectId, duration = 0) { + super(ship); - this.effect = (effect instanceof BaseEffect) ? effect.id : effect; - this.duration = duration; - } - - protected applyOnShip(ship: Ship, battle: Battle): void { - let effect = ship.active_effects.get(this.effect); - if (effect) { - if (this.duration) { - if (effect instanceof StickyEffect) { - effect.duration += this.duration; - } else { - console.error("Could not apply diff - not a sticky effect", this, ship); - } - } - } else { - console.error("Could not apply diff - effect not found on ship", this, ship); - } - } - - protected getReverse(): BaseBattleDiff { - return new ShipEffectChangedDiff(this.ship_id, this.effect, -this.duration); + this.effect = (effect instanceof BaseEffect) ? effect.id : effect; + this.duration = duration; + } + + protected applyOnShip(ship: Ship, battle: Battle): void { + let effect = ship.active_effects.get(this.effect); + if (effect) { + if (this.duration) { + if (effect instanceof StickyEffect) { + effect.duration += this.duration; + } else { + console.error("Could not apply diff - not a sticky effect", this, ship); } + } + } else { + console.error("Could not apply diff - effect not found on ship", this, ship); } + } + + protected getReverse(): BaseBattleDiff { + return new ShipEffectChangedDiff(this.ship_id, this.effect, -this.duration); + } } diff --git a/src/core/diffs/ShipMoveDiff.spec.ts b/src/core/diffs/ShipMoveDiff.spec.ts index 3054df5..b3ab6f6 100644 --- a/src/core/diffs/ShipMoveDiff.spec.ts +++ b/src/core/diffs/ShipMoveDiff.spec.ts @@ -1,17 +1,15 @@ -module TK.SpaceTac.Specs { - testing("ShipMoveDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - check.equals(ship.location, new ArenaLocationAngle(0, 0, 0)); +testing("ShipMoveDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + check.equals(ship.location, new ArenaLocationAngle(0, 0, 0)); - let engine = new MoveAction(); - let event = new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(50, 20, 1.2), engine); - event.apply(battle); - check.equals(ship.location, new ArenaLocationAngle(50, 20, 1.2)); + let engine = new MoveAction(); + let event = new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(50, 20, 1.2), engine); + event.apply(battle); + check.equals(ship.location, new ArenaLocationAngle(50, 20, 1.2)); - event.revert(battle); - check.equals(ship.location, new ArenaLocationAngle(0, 0, 0)); - }); - }); -} \ No newline at end of file + event.revert(battle); + check.equals(ship.location, new ArenaLocationAngle(0, 0, 0)); + }); +}); diff --git a/src/core/diffs/ShipMoveDiff.ts b/src/core/diffs/ShipMoveDiff.ts index 62183bc..8e5a875 100644 --- a/src/core/diffs/ShipMoveDiff.ts +++ b/src/core/diffs/ShipMoveDiff.ts @@ -1,41 +1,44 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { MoveAction } from "../actions/MoveAction"; +import { arenaDistance, ArenaLocationAngle } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship moves in the arena - */ - export class ShipMoveDiff extends BaseBattleShipDiff { - // Previous location - start: ArenaLocationAngle +/** + * A ship moves in the arena + */ +export class ShipMoveDiff extends BaseBattleShipDiff { + // Previous location + start: ArenaLocationAngle - // New location - end: ArenaLocationAngle + // New location + end: ArenaLocationAngle - // Engine used - engine: MoveAction | null + // Engine used + engine: MoveAction | null - constructor(ship: Ship | RObjectId, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: MoveAction | null = null) { - super(ship); + constructor(ship: Ship | RObjectId, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: MoveAction | null = null) { + super(ship); - this.start = start; - this.end = end; - this.engine = engine; - } + this.start = start; + this.end = end; + this.engine = engine; + } - /** - * Get the distance travelled - */ - getDistance(): number { - return arenaDistance(this.start, this.end); - } + /** + * Get the distance travelled + */ + getDistance(): number { + return arenaDistance(this.start, this.end); + } - applyOnShip(ship: Ship, battle: Battle) { - ship.setArenaPosition(this.end.x, this.end.y); - ship.setArenaFacingAngle(this.end.angle); - } + applyOnShip(ship: Ship, battle: Battle) { + ship.setArenaPosition(this.end.x, this.end.y); + ship.setArenaFacingAngle(this.end.angle); + } - getReverse(): ShipMoveDiff { - return new ShipMoveDiff(this.ship_id, this.end, this.start, this.engine); - } - } + getReverse(): ShipMoveDiff { + return new ShipMoveDiff(this.ship_id, this.end, this.start, this.engine); + } } diff --git a/src/core/diffs/ShipValueDiff.spec.ts b/src/core/diffs/ShipValueDiff.spec.ts index 0d94a8d..49daa0c 100644 --- a/src/core/diffs/ShipValueDiff.spec.ts +++ b/src/core/diffs/ShipValueDiff.spec.ts @@ -1,23 +1,21 @@ -module TK.SpaceTac.Specs { - testing("ShipValueDiff", test => { - test.case("applies and reverts", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); +testing("ShipValueDiff", test => { + test.case("applies and reverts", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); - TestTools.diffChain(check, battle, [ - new ShipValueDiff(ship, "hull", 15), - new ShipValueDiff(ship, "hull", -7) - ], [ - check => { - check.equals(ship.getValue("hull"), 0, "hull value"); - }, - check => { - check.equals(ship.getValue("hull"), 15, "hull value"); - }, - check => { - check.equals(ship.getValue("hull"), 8, "hull value"); - }, - ]) - }); - }); -} \ No newline at end of file + TestTools.diffChain(check, battle, [ + new ShipValueDiff(ship, "hull", 15), + new ShipValueDiff(ship, "hull", -7) + ], [ + check => { + check.equals(ship.getValue("hull"), 0, "hull value"); + }, + check => { + check.equals(ship.getValue("hull"), 15, "hull value"); + }, + check => { + check.equals(ship.getValue("hull"), 8, "hull value"); + }, + ]) + }); +}); diff --git a/src/core/diffs/ShipValueDiff.ts b/src/core/diffs/ShipValueDiff.ts index 14b8782..280bd3e 100644 --- a/src/core/diffs/ShipValueDiff.ts +++ b/src/core/diffs/ShipValueDiff.ts @@ -1,29 +1,31 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { Battle } from "../Battle"; +import { Ship } from "../Ship"; +import { ShipValues } from "../ShipValue"; +import { BaseBattleDiff, BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A ship value changed - */ - export class ShipValueDiff extends BaseBattleShipDiff { - // Value that changes - code: keyof ShipValues +/** + * A ship value changed + */ +export class ShipValueDiff extends BaseBattleShipDiff { + // Value that changes + code: keyof ShipValues - // Value variation - diff: number + // Value variation + diff: number - constructor(ship: Ship | RObjectId, code: keyof ShipValues, diff: number) { - super(ship); + constructor(ship: Ship | RObjectId, code: keyof ShipValues, diff: number) { + super(ship); - this.code = code; - this.diff = diff; - } + this.code = code; + this.diff = diff; + } - getReverse(): BaseBattleDiff { - return new ShipValueDiff(this.ship_id, this.code, -this.diff); - } + getReverse(): BaseBattleDiff { + return new ShipValueDiff(this.ship_id, this.code, -this.diff); + } - applyOnShip(ship: Ship, battle: Battle): void { - ship.values[this.code] += this.diff; - } - } + applyOnShip(ship: Ship, battle: Battle): void { + ship.values[this.code] += this.diff; + } } diff --git a/src/core/diffs/VigilanceAppliedDiff.ts b/src/core/diffs/VigilanceAppliedDiff.ts index a8d5649..dde8e98 100644 --- a/src/core/diffs/VigilanceAppliedDiff.ts +++ b/src/core/diffs/VigilanceAppliedDiff.ts @@ -1,20 +1,21 @@ -/// +import { RObjectId } from "../../common/RObject"; +import { VigilanceAction } from "../actions/VigilanceAction"; +import { Ship } from "../Ship"; +import { BaseBattleShipDiff } from "./BaseBattleDiff"; -module TK.SpaceTac { - /** - * A vigilance reaction has been triggered - * - * This does not do anything, and is just there for animations - */ - export class VigilanceAppliedDiff extends BaseBattleShipDiff { - action: RObjectId - target: RObjectId +/** + * A vigilance reaction has been triggered + * + * This does not do anything, and is just there for animations + */ +export class VigilanceAppliedDiff extends BaseBattleShipDiff { + action: RObjectId + target: RObjectId - constructor(source: Ship, action: VigilanceAction, target: Ship) { - super(source); + constructor(source: Ship, action: VigilanceAction, target: Ship) { + super(source); - this.action = action.id; - this.target = target.id; - } - } + this.action = action.id; + this.target = target.id; + } } diff --git a/src/core/effects/AttributeEffect.spec.ts b/src/core/effects/AttributeEffect.spec.ts index 95a2c57..41512d7 100644 --- a/src/core/effects/AttributeEffect.spec.ts +++ b/src/core/effects/AttributeEffect.spec.ts @@ -1,31 +1,29 @@ -module TK.SpaceTac { - testing("AttributeEffect", test => { - test.case("applies cumulatively on attribute", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - check.equals(ship.getAttribute("evasion"), 0, "initial"); +testing("AttributeEffect", test => { + test.case("applies cumulatively on attribute", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + check.equals(ship.getAttribute("evasion"), 0, "initial"); - let effect1 = new AttributeEffect("evasion", 20); - battle.applyDiffs(effect1.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("evasion"), 20, "applied 1"); + let effect1 = new AttributeEffect("evasion", 20); + battle.applyDiffs(effect1.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("evasion"), 20, "applied 1"); - let effect2 = new AttributeEffect("evasion", 10); - battle.applyDiffs(effect2.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("evasion"), 30, "applied 2"); + let effect2 = new AttributeEffect("evasion", 10); + battle.applyDiffs(effect2.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("evasion"), 30, "applied 2"); - battle.applyDiffs(effect1.getOffDiffs(ship)); - check.equals(ship.getAttribute("evasion"), 10, "reverted 1"); + battle.applyDiffs(effect1.getOffDiffs(ship)); + check.equals(ship.getAttribute("evasion"), 10, "reverted 1"); - battle.applyDiffs(effect2.getOffDiffs(ship)); - check.equals(ship.getAttribute("evasion"), 0, "reverted 2"); - }); + battle.applyDiffs(effect2.getOffDiffs(ship)); + check.equals(ship.getAttribute("evasion"), 0, "reverted 2"); + }); - test.case("has a description", check => { - let effect = new AttributeEffect("evasion", 12); - check.equals(effect.getDescription(), "evasion +12"); + test.case("has a description", check => { + let effect = new AttributeEffect("evasion", 12); + check.equals(effect.getDescription(), "evasion +12"); - effect = new AttributeEffect("shield_capacity", -4); - check.equals(effect.getDescription(), "shield capacity -4"); - }); - }); -} + effect = new AttributeEffect("shield_capacity", -4); + check.equals(effect.getDescription(), "shield capacity -4"); + }); +}); diff --git a/src/core/effects/AttributeEffect.ts b/src/core/effects/AttributeEffect.ts index 0e84d37..a92922f 100644 --- a/src/core/effects/AttributeEffect.ts +++ b/src/core/effects/AttributeEffect.ts @@ -1,48 +1,51 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipAttributeDiff } from "../diffs/ShipAttributeDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { ShipAttributes, SHIP_VALUES_NAMES } from "../ShipValue"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Effect to modify an attribute. - * - * Attribute effects are stacking, and the value of an attribute is in fact the sum of all active attribute effects. - */ - export class AttributeEffect extends BaseEffect { - // Affected attribute - attrcode: keyof ShipAttributes +/** + * Effect to modify an attribute. + * + * Attribute effects are stacking, and the value of an attribute is in fact the sum of all active attribute effects. + */ +export class AttributeEffect extends BaseEffect { + // Affected attribute + attrcode: keyof ShipAttributes - // Base value - value: number + // Base value + value: number - constructor(attrcode: keyof ShipAttributes, value = 0) { - super("attr"); + constructor(attrcode: keyof ShipAttributes, value = 0) { + super("attr"); - this.attrcode = attrcode; - this.value = value; - } + this.attrcode = attrcode; + this.value = value; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, { cumulative: this.value }, {}), - ]; - } + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, { cumulative: this.value }, {}), + ]; + } - getOffDiffs(ship: Ship): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, {}, { cumulative: this.value }), - ]; - } + getOffDiffs(ship: Ship): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, {}, { cumulative: this.value }), + ]; + } - isBeneficial(): boolean { - return this.value >= 0; - } + isBeneficial(): boolean { + return this.value >= 0; + } - getFullCode(): string { - return this.code + "-" + this.attrcode; - } + getFullCode(): string { + return this.code + "-" + this.attrcode; + } - getDescription(): string { - let attrname = SHIP_VALUES_NAMES[this.attrcode]; - return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}`; - } - } + getDescription(): string { + let attrname = SHIP_VALUES_NAMES[this.attrcode]; + return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}`; + } } diff --git a/src/core/effects/AttributeLimitEffect.spec.ts b/src/core/effects/AttributeLimitEffect.spec.ts index fcff3d4..d3bacfe 100644 --- a/src/core/effects/AttributeLimitEffect.spec.ts +++ b/src/core/effects/AttributeLimitEffect.spec.ts @@ -1,29 +1,27 @@ -module TK.SpaceTac { - testing("AttributeLimitEffect", test => { - test.case("applies cumulatively on attribute", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - ship.attributes.evasion.addModifier(12); - check.equals(ship.getAttribute("evasion"), 12, "initial"); +testing("AttributeLimitEffect", test => { + test.case("applies cumulatively on attribute", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + ship.attributes.evasion.addModifier(12); + check.equals(ship.getAttribute("evasion"), 12, "initial"); - let effect1 = new AttributeLimitEffect("evasion", 5); - battle.applyDiffs(effect1.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("evasion"), 5, "applied 1"); + let effect1 = new AttributeLimitEffect("evasion", 5); + battle.applyDiffs(effect1.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("evasion"), 5, "applied 1"); - let effect2 = new AttributeLimitEffect("evasion", 3); - battle.applyDiffs(effect2.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("evasion"), 3, "applied 2"); + let effect2 = new AttributeLimitEffect("evasion", 3); + battle.applyDiffs(effect2.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("evasion"), 3, "applied 2"); - battle.applyDiffs(effect1.getOffDiffs(ship)); - check.equals(ship.getAttribute("evasion"), 3, "reverted 1"); + battle.applyDiffs(effect1.getOffDiffs(ship)); + check.equals(ship.getAttribute("evasion"), 3, "reverted 1"); - battle.applyDiffs(effect2.getOffDiffs(ship)); - check.equals(ship.getAttribute("evasion"), 12, "reverted 2"); - }); + battle.applyDiffs(effect2.getOffDiffs(ship)); + check.equals(ship.getAttribute("evasion"), 12, "reverted 2"); + }); - test.case("has a description", check => { - let effect = new AttributeLimitEffect("power_capacity", 4); - check.equals(effect.getDescription(), "limit power capacity to 4"); - }); - }); -} + test.case("has a description", check => { + let effect = new AttributeLimitEffect("power_capacity", 4); + check.equals(effect.getDescription(), "limit power capacity to 4"); + }); +}); diff --git a/src/core/effects/AttributeLimitEffect.ts b/src/core/effects/AttributeLimitEffect.ts index fefc2ae..0c9443e 100644 --- a/src/core/effects/AttributeLimitEffect.ts +++ b/src/core/effects/AttributeLimitEffect.ts @@ -1,48 +1,51 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipAttributeDiff } from "../diffs/ShipAttributeDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { ShipAttributes, SHIP_VALUES_NAMES } from "../ShipValue"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Enforce a limitation on ship attribute final value - * - * For example, this could be used to slow a target by limiting its action points - */ - export class AttributeLimitEffect extends BaseEffect { - // Affected attribute - attrcode: keyof ShipAttributes; +/** + * Enforce a limitation on ship attribute final value + * + * For example, this could be used to slow a target by limiting its action points + */ +export class AttributeLimitEffect extends BaseEffect { + // Affected attribute + attrcode: keyof ShipAttributes; - // Limit of the attribute value - value: number; + // Limit of the attribute value + value: number; - constructor(attrcode: keyof ShipAttributes, value = 0) { - super("attrlimit"); + constructor(attrcode: keyof ShipAttributes, value = 0) { + super("attrlimit"); - this.attrcode = attrcode; - this.value = value; - } + this.attrcode = attrcode; + this.value = value; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, { limit: this.value }, {}), - ]; - } + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, { limit: this.value }, {}), + ]; + } - getOffDiffs(ship: Ship): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, {}, { limit: this.value }), - ]; - } + getOffDiffs(ship: Ship): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, {}, { limit: this.value }), + ]; + } - isBeneficial(): boolean { - return false; - } + isBeneficial(): boolean { + return false; + } - getFullCode(): string { - return this.code + "-" + this.attrcode; - } + getFullCode(): string { + return this.code + "-" + this.attrcode; + } - getDescription(): string { - let attrname = SHIP_VALUES_NAMES[this.attrcode]; - return `limit ${attrname} to ${this.value}`; - } - } + getDescription(): string { + let attrname = SHIP_VALUES_NAMES[this.attrcode]; + return `limit ${attrname} to ${this.value}`; + } } diff --git a/src/core/effects/AttributeMultiplyEffect.spec.ts b/src/core/effects/AttributeMultiplyEffect.spec.ts index ef5e582..e1d12a5 100644 --- a/src/core/effects/AttributeMultiplyEffect.spec.ts +++ b/src/core/effects/AttributeMultiplyEffect.spec.ts @@ -1,29 +1,27 @@ -module TK.SpaceTac { - testing("AttributeMultiplyEffect", test => { - test.case("boosts or reduces cumulatively an attribute", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - ship.attributes.hull_capacity.addModifier(100); - check.equals(ship.getAttribute("hull_capacity"), 100, "initial"); +testing("AttributeMultiplyEffect", test => { + test.case("boosts or reduces cumulatively an attribute", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + ship.attributes.hull_capacity.addModifier(100); + check.equals(ship.getAttribute("hull_capacity"), 100, "initial"); - let effect1 = new AttributeMultiplyEffect("hull_capacity", 30); - battle.applyDiffs(effect1.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("hull_capacity"), 130, "applied 1"); + let effect1 = new AttributeMultiplyEffect("hull_capacity", 30); + battle.applyDiffs(effect1.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("hull_capacity"), 130, "applied 1"); - let effect2 = new AttributeMultiplyEffect("hull_capacity", -10); - battle.applyDiffs(effect2.getOnDiffs(ship, ship)); - check.equals(ship.getAttribute("hull_capacity"), 120, "applied 2"); + let effect2 = new AttributeMultiplyEffect("hull_capacity", -10); + battle.applyDiffs(effect2.getOnDiffs(ship, ship)); + check.equals(ship.getAttribute("hull_capacity"), 120, "applied 2"); - battle.applyDiffs(effect1.getOffDiffs(ship)); - check.equals(ship.getAttribute("hull_capacity"), 90, "reverted 1"); + battle.applyDiffs(effect1.getOffDiffs(ship)); + check.equals(ship.getAttribute("hull_capacity"), 90, "reverted 1"); - battle.applyDiffs(effect2.getOffDiffs(ship)); - check.equals(ship.getAttribute("hull_capacity"), 100, "reverted 2"); - }); + battle.applyDiffs(effect2.getOffDiffs(ship)); + check.equals(ship.getAttribute("hull_capacity"), 100, "reverted 2"); + }); - test.case("has a description", check => { - let effect = new AttributeMultiplyEffect("power_capacity", 20); - check.equals(effect.getDescription(), "power capacity +20%"); - }); - }); -} + test.case("has a description", check => { + let effect = new AttributeMultiplyEffect("power_capacity", 20); + check.equals(effect.getDescription(), "power capacity +20%"); + }); +}); diff --git a/src/core/effects/AttributeMultiplyEffect.ts b/src/core/effects/AttributeMultiplyEffect.ts index 78bb058..d82e716 100644 --- a/src/core/effects/AttributeMultiplyEffect.ts +++ b/src/core/effects/AttributeMultiplyEffect.ts @@ -1,49 +1,52 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipAttributeDiff } from "../diffs/ShipAttributeDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { ShipAttributes, SHIP_VALUES_NAMES } from "../ShipValue"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Boost or reduce an attribute value - * - * This effect is stored as "20" for "+20%", or "-10" for "-10%". - * Several multiply effects are cumulative (+20 and +10 will apply a +30 boost). - */ - export class AttributeMultiplyEffect extends BaseEffect { - // Affected attribute - attrcode: keyof ShipAttributes; +/** + * Boost or reduce an attribute value + * + * This effect is stored as "20" for "+20%", or "-10" for "-10%". + * Several multiply effects are cumulative (+20 and +10 will apply a +30 boost). + */ +export class AttributeMultiplyEffect extends BaseEffect { + // Affected attribute + attrcode: keyof ShipAttributes; - // Boost factor (percentage) - value: number; + // Boost factor (percentage) + value: number; - constructor(attrcode: keyof ShipAttributes, value = 0) { - super("attrmult"); + constructor(attrcode: keyof ShipAttributes, value = 0) { + super("attrmult"); - this.attrcode = attrcode; - this.value = value; - } + this.attrcode = attrcode; + this.value = value; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, { multiplier: this.value }, {}), - ]; - } + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, { multiplier: this.value }, {}), + ]; + } - getOffDiffs(ship: Ship): BaseBattleDiff[] { - return [ - new ShipAttributeDiff(ship, this.attrcode, {}, { multiplier: this.value }), - ]; - } + getOffDiffs(ship: Ship): BaseBattleDiff[] { + return [ + new ShipAttributeDiff(ship, this.attrcode, {}, { multiplier: this.value }), + ]; + } - isBeneficial(): boolean { - return false; - } + isBeneficial(): boolean { + return false; + } - getFullCode(): string { - return this.code + "-" + this.attrcode; - } + getFullCode(): string { + return this.code + "-" + this.attrcode; + } - getDescription(): string { - let attrname = SHIP_VALUES_NAMES[this.attrcode]; - return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}%`; - } - } + getDescription(): string { + let attrname = SHIP_VALUES_NAMES[this.attrcode]; + return `${attrname} ${this.value > 0 ? "+" : "-"}${Math.abs(this.value)}%`; + } } diff --git a/src/core/effects/BaseEffect.ts b/src/core/effects/BaseEffect.ts index f0b8a65..d115950 100644 --- a/src/core/effects/BaseEffect.ts +++ b/src/core/effects/BaseEffect.ts @@ -1,75 +1,76 @@ -/// +import { RObject } from "../../common/RObject"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; -module TK.SpaceTac { - /** - * Base class for effects of actions that can be applied on ships - * - * Effects will generate diffs to modify the battle state - */ - export class BaseEffect extends RObject { - // Identifier code for the type of effect - code: string +/** + * Base class for effects of actions that can be applied on ships + * + * Effects will generate diffs to modify the battle state + */ +export class BaseEffect extends RObject { + // Identifier code for the type of effect + code: string - constructor(code: string) { - super(); + constructor(code: string) { + super(); - this.code = code; - } + this.code = code; + } - /** - * Get the list of diffs needed to activate this effect on a ship - */ - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - return []; - } + /** + * Get the list of diffs needed to activate this effect on a ship + */ + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + return []; + } - /** - * Get the list of diffs needed to remove this effect on a ship - */ - getOffDiffs(ship: Ship): BaseBattleDiff[] { - return []; - } + /** + * Get the list of diffs needed to remove this effect on a ship + */ + getOffDiffs(ship: Ship): BaseBattleDiff[] { + return []; + } - /** - * Get the list of diffs to apply when this effect is active on a ship beginning its turn - */ - getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { - return []; - } + /** + * Get the list of diffs to apply when this effect is active on a ship beginning its turn + */ + getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { + return []; + } - /** - * Get the list of diffs to apply when this effect is active on a ship ending its turn - */ - getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { - return []; - } + /** + * Get the list of diffs to apply when this effect is active on a ship ending its turn + */ + getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { + return []; + } - /** - * Return true if the effect is internal and should not be displayed to the players - */ - isInternal(): boolean { - return false; - } + /** + * Return true if the effect is internal and should not be displayed to the players + */ + isInternal(): boolean { + return false; + } - /** - * Return true if the effect is beneficial to the ship, false if it's a drawback - */ - isBeneficial(): boolean { - return false; - } + /** + * Return true if the effect is beneficial to the ship, false if it's a drawback + */ + isBeneficial(): boolean { + return false; + } - /** - * Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery") - */ - getFullCode(): string { - return this.code; - } + /** + * Get a full code, that can be used to identify this effect (for example: "attrlimit-aprecovery") + */ + getFullCode(): string { + return this.code; + } - /** - * Return a human readable description - */ - getDescription(): string { - return "unknown effect"; - } - } + /** + * Return a human readable description + */ + getDescription(): string { + return "unknown effect"; + } } diff --git a/src/core/effects/CooldownEffect.spec.ts b/src/core/effects/CooldownEffect.spec.ts index f2a5e61..e6f0632 100644 --- a/src/core/effects/CooldownEffect.spec.ts +++ b/src/core/effects/CooldownEffect.spec.ts @@ -1,38 +1,36 @@ -module TK.SpaceTac { - testing("CooldownEffect", test => { - test.case("cools down equipment", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let weapons = [TestTools.addWeapon(ship), TestTools.addWeapon(ship), TestTools.addWeapon(ship)]; - weapons.forEach(weapon => weapon.configureCooldown(1, 3)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); +testing("CooldownEffect", test => { + test.case("cools down equipment", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapons = [TestTools.addWeapon(ship), TestTools.addWeapon(ship), TestTools.addWeapon(ship)]; + weapons.forEach(weapon => weapon.configureCooldown(1, 3)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); - let effect = new CooldownEffect(0, 0); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); + let effect = new CooldownEffect(0, 0); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); - weapons.forEach(weapon => ship.actions.storeUsage(weapon)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); + weapons.forEach(weapon => ship.actions.storeUsage(weapon)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); - weapons.forEach(weapon => ship.actions.storeUsage(weapon)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); + weapons.forEach(weapon => ship.actions.storeUsage(weapon)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); - effect = new CooldownEffect(1, 0); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [2, 2, 2]); + effect = new CooldownEffect(1, 0); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [2, 2, 2]); - effect = new CooldownEffect(1, 2); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat).sort(), [1, 1, 2]); - }) + effect = new CooldownEffect(1, 2); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat).sort(), [1, 1, 2]); + }) - test.case("builds a textual description", check => { - check.equals(new CooldownEffect(0, 0).getDescription(), "full cooling (all equipments)"); - check.equals(new CooldownEffect(1, 1).getDescription(), "1 cooling (1 equipment)"); - check.equals(new CooldownEffect(2, 2).getDescription(), "2 cooling (2 equipments)"); - }) - }) -} + test.case("builds a textual description", check => { + check.equals(new CooldownEffect(0, 0).getDescription(), "full cooling (all equipments)"); + check.equals(new CooldownEffect(1, 1).getDescription(), "1 cooling (1 equipment)"); + check.equals(new CooldownEffect(2, 2).getDescription(), "2 cooling (2 equipments)"); + }) +}) diff --git a/src/core/effects/CooldownEffect.ts b/src/core/effects/CooldownEffect.ts index 2f525c1..5991a7e 100644 --- a/src/core/effects/CooldownEffect.ts +++ b/src/core/effects/CooldownEffect.ts @@ -1,40 +1,43 @@ -/// +import { RandomGenerator } from "../../common/RandomGenerator"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipCooldownDiff } from "../diffs/ShipCooldownDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Cools down equipment of affected ships - */ - export class CooldownEffect extends BaseEffect { - // Number of cooling steps to apply - cooling: number +/** + * Cools down equipment of affected ships + */ +export class CooldownEffect extends BaseEffect { + // Number of cooling steps to apply + cooling: number - // Maximal number of equipment to cool on one ship (will be chosen at random) - maxcount: number + // Maximal number of equipment to cool on one ship (will be chosen at random) + maxcount: number - constructor(cooling = 0, maxcount = 0) { - super("cooldown"); + constructor(cooling = 0, maxcount = 0) { + super("cooldown"); - this.cooling = cooling; - this.maxcount = maxcount; - } + this.cooling = cooling; + this.maxcount = maxcount; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - let actions = ship.actions.listOverheated(); + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + let actions = ship.actions.listOverheated(); - if (this.maxcount && actions.length > this.maxcount) { - let random = RandomGenerator.global; - actions = random.sample(actions, this.maxcount); - } - - return actions.map(action => new ShipCooldownDiff(ship, action, this.cooling || ship.actions.getCooldown(action).heat)); - } - - isBeneficial(): boolean { - return true; - } - - getDescription(): string { - return `${this.cooling ? this.cooling : "full"} cooling (${this.maxcount ? this.maxcount : "all"} equipment${this.maxcount != 1 ? "s" : ""})`; - } + if (this.maxcount && actions.length > this.maxcount) { + let random = RandomGenerator.global; + actions = random.sample(actions, this.maxcount); } + + return actions.map(action => new ShipCooldownDiff(ship, action, this.cooling || ship.actions.getCooldown(action).heat)); + } + + isBeneficial(): boolean { + return true; + } + + getDescription(): string { + return `${this.cooling ? this.cooling : "full"} cooling (${this.maxcount ? this.maxcount : "all"} equipment${this.maxcount != 1 ? "s" : ""})`; + } } diff --git a/src/core/effects/DamageEffect.spec.ts b/src/core/effects/DamageEffect.spec.ts index 14c6990..2defd8b 100644 --- a/src/core/effects/DamageEffect.spec.ts +++ b/src/core/effects/DamageEffect.spec.ts @@ -1,63 +1,61 @@ -module TK.SpaceTac.Specs { - testing("DamageEffect", test => { - test.case("computes shield and hull damage, according to mode", check => { - let ship = new Ship(); - TestTools.setShipModel(ship, 2, 3); +testing("DamageEffect", test => { + test.case("computes shield and hull damage, according to mode", check => { + let ship = new Ship(); + TestTools.setShipModel(ship, 2, 3); - check.equals(new DamageEffect(1, DamageEffectMode.HULL_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 0, 0, 1), "hull 1"); - check.equals(new DamageEffect(3, DamageEffectMode.HULL_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 0, 0, 3), "hull 3"); + check.equals(new DamageEffect(1, DamageEffectMode.HULL_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 0, 0, 1), "hull 1"); + check.equals(new DamageEffect(3, DamageEffectMode.HULL_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 0, 0, 3), "hull 3"); - check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "shield 1"); - check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 3, 0, 4), "shield 4"); + check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "shield 1"); + check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_ONLY).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 3, 0, 4), "shield 4"); - check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "piercing 1"); - check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 3, 0, 4), "piercing 4"); - check.equals(new DamageEffect(8, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 3, 0, 8), "piercing 8"); + check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "piercing 1"); + check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 3, 0, 4), "piercing 4"); + check.equals(new DamageEffect(8, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 3, 0, 8), "piercing 8"); - check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "normal 1"); - check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 3, 0, 4), "normal 4"); - ship.setValue("shield", 0); - check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 0, 0, 1), "normal no shield 1"); - check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 0, 0, 4), "normal no shield 4"); + check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 1, 0, 1), "normal 1"); + check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 0, 3, 0, 4), "normal 4"); + ship.setValue("shield", 0); + check.equals(new DamageEffect(1, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 0, 0, 1), "normal no shield 1"); + check.equals(new DamageEffect(4, DamageEffectMode.SHIELD_OR_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 2, 0, 0, 4), "normal no shield 4"); - ship.setValue("shield", 3); - TestTools.setAttribute(ship, "evasion", 1); - check.equals(new DamageEffect(5, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 3, 1, 5), "piercing 5 evade 1"); - }); + ship.setValue("shield", 3); + TestTools.setAttribute(ship, "evasion", 1); + check.equals(new DamageEffect(5, DamageEffectMode.SHIELD_THEN_HULL).getEffectiveDamage(ship), new ShipDamageDiff(ship, 1, 3, 1, 5), "piercing 5 evade 1"); + }); - test.case("applies damage", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - TestTools.setShipModel(ship, 150, 400); + test.case("applies damage", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 150, 400); - function checkValues(desc: string, hull_value: number, shield_value: number) { - check.in(desc, check => { - check.equals(ship.getValue("hull"), hull_value, "hull value"); - check.equals(ship.getValue("shield"), shield_value, "shield value"); - }); - } + function checkValues(desc: string, hull_value: number, shield_value: number) { + check.in(desc, check => { + check.equals(ship.getValue("hull"), hull_value, "hull value"); + check.equals(ship.getValue("shield"), shield_value, "shield value"); + }); + } - checkValues("initial", 150, 400); + checkValues("initial", 150, 400); - battle.applyDiffs(new DamageEffect(50, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); - checkValues("after 50 damage", 150, 350); + battle.applyDiffs(new DamageEffect(50, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); + checkValues("after 50 damage", 150, 350); - battle.applyDiffs(new DamageEffect(250, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); - checkValues("after 250 damage", 150, 100); + battle.applyDiffs(new DamageEffect(250, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); + checkValues("after 250 damage", 150, 100); - battle.applyDiffs(new DamageEffect(201, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); - checkValues("after 201 damage", 49, 0); + battle.applyDiffs(new DamageEffect(201, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); + checkValues("after 201 damage", 49, 0); - battle.applyDiffs(new DamageEffect(8000, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); - checkValues("after 8000 damage", 0, 0); - }); + battle.applyDiffs(new DamageEffect(8000, DamageEffectMode.SHIELD_THEN_HULL).getOnDiffs(ship, ship)); + checkValues("after 8000 damage", 0, 0); + }); - test.case("gets a textual description", check => { - check.equals(new DamageEffect(10).getDescription(), "do 10 damage"); - check.equals(new DamageEffect(10, DamageEffectMode.HULL_ONLY).getDescription(), "do 10 hull damage"); - check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_ONLY).getDescription(), "do 10 shield damage"); - check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_THEN_HULL).getDescription(), "do 10 piercing damage"); - check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_ONLY, false).getDescription(), "do 10 unevadable shield damage"); - }); - }); -} + test.case("gets a textual description", check => { + check.equals(new DamageEffect(10).getDescription(), "do 10 damage"); + check.equals(new DamageEffect(10, DamageEffectMode.HULL_ONLY).getDescription(), "do 10 hull damage"); + check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_ONLY).getDescription(), "do 10 shield damage"); + check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_THEN_HULL).getDescription(), "do 10 piercing damage"); + check.equals(new DamageEffect(10, DamageEffectMode.SHIELD_ONLY, false).getDescription(), "do 10 unevadable shield damage"); + }); +}); diff --git a/src/core/effects/DamageEffect.ts b/src/core/effects/DamageEffect.ts index b69c43e..cabb7e2 100644 --- a/src/core/effects/DamageEffect.ts +++ b/src/core/effects/DamageEffect.ts @@ -1,109 +1,112 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipDamageDiff } from "../diffs/ShipDamageDiff"; +import { ShipValueDiff } from "../diffs/ShipValueDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Mode for damage effect - */ - export enum DamageEffectMode { - // Apply on shield only - SHIELD_ONLY, - // Apply on shield, then remaining value on hull - SHIELD_THEN_HULL, - // Apply on shield only if up, otherwise on hull - SHIELD_OR_HULL, - // Apply on hull only - HULL_ONLY - } - - /** - * Apply damage on a ship. - */ - export class DamageEffect extends BaseEffect { - // Damage amount - value: number - - // Damage mode - mode: DamageEffectMode - - // Evadable damage (applies evasion) - evadable: boolean - - constructor(value: number, mode = DamageEffectMode.SHIELD_OR_HULL, evadable = true) { - super("damage"); - - this.value = value; - this.mode = mode; - this.evadable = evadable; - } - - /** - * Get the effective damage done to both shield and hull (in this order) - */ - getEffectiveDamage(ship: Ship): ShipDamageDiff { - let shield = ship.getValue("shield"); - let hull = ship.getValue("hull"); - let dhull = 0; - let dshield = 0; - - // Apply evasion - let evaded = this.evadable ? Math.min(this.value, ship.getAttribute("evasion")) : 0; - let damage = this.value - evaded; - - // Split in shield/hull damage - if (this.mode == DamageEffectMode.HULL_ONLY) { - dhull = Math.min(damage, hull); - } else if (this.mode == DamageEffectMode.SHIELD_ONLY) { - dshield = Math.min(damage, shield); - } else if (this.mode == DamageEffectMode.SHIELD_OR_HULL) { - if (shield) { - dshield = Math.min(damage, shield); - } else { - dhull = Math.min(damage, hull); - } - } else { - dshield = Math.min(damage, shield); - dhull = Math.min(damage - dshield, hull); - } - - return new ShipDamageDiff(ship, dhull, dshield, evaded, this.value); - } - - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - let damage = this.getEffectiveDamage(ship); - - if (damage.shield || damage.hull) { - result.push(damage); - } - - if (damage.shield) { - result.push(new ShipValueDiff(ship, "shield", -damage.shield)); - } - - if (damage.hull) { - result.push(new ShipValueDiff(ship, "hull", -damage.hull)); - } - - return result; - } - - getDescription(): string { - let mode = ""; - if (this.mode == DamageEffectMode.HULL_ONLY) { - mode = " hull"; - } else if (this.mode == DamageEffectMode.SHIELD_ONLY) { - mode = " shield"; - } else if (this.mode == DamageEffectMode.SHIELD_THEN_HULL) { - mode = " piercing"; - } - - let modifier = ""; - if (!this.evadable) { - modifier = " unevadable"; - } - - return `do ${this.value}${modifier}${mode} damage`; - } - } +/** + * Mode for damage effect + */ +export enum DamageEffectMode { + // Apply on shield only + SHIELD_ONLY, + // Apply on shield, then remaining value on hull + SHIELD_THEN_HULL, + // Apply on shield only if up, otherwise on hull + SHIELD_OR_HULL, + // Apply on hull only + HULL_ONLY +} + +/** + * Apply damage on a ship. + */ +export class DamageEffect extends BaseEffect { + // Damage amount + value: number + + // Damage mode + mode: DamageEffectMode + + // Evadable damage (applies evasion) + evadable: boolean + + constructor(value: number, mode = DamageEffectMode.SHIELD_OR_HULL, evadable = true) { + super("damage"); + + this.value = value; + this.mode = mode; + this.evadable = evadable; + } + + /** + * Get the effective damage done to both shield and hull (in this order) + */ + getEffectiveDamage(ship: Ship): ShipDamageDiff { + let shield = ship.getValue("shield"); + let hull = ship.getValue("hull"); + let dhull = 0; + let dshield = 0; + + // Apply evasion + let evaded = this.evadable ? Math.min(this.value, ship.getAttribute("evasion")) : 0; + let damage = this.value - evaded; + + // Split in shield/hull damage + if (this.mode == DamageEffectMode.HULL_ONLY) { + dhull = Math.min(damage, hull); + } else if (this.mode == DamageEffectMode.SHIELD_ONLY) { + dshield = Math.min(damage, shield); + } else if (this.mode == DamageEffectMode.SHIELD_OR_HULL) { + if (shield) { + dshield = Math.min(damage, shield); + } else { + dhull = Math.min(damage, hull); + } + } else { + dshield = Math.min(damage, shield); + dhull = Math.min(damage - dshield, hull); + } + + return new ShipDamageDiff(ship, dhull, dshield, evaded, this.value); + } + + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + let damage = this.getEffectiveDamage(ship); + + if (damage.shield || damage.hull) { + result.push(damage); + } + + if (damage.shield) { + result.push(new ShipValueDiff(ship, "shield", -damage.shield)); + } + + if (damage.hull) { + result.push(new ShipValueDiff(ship, "hull", -damage.hull)); + } + + return result; + } + + getDescription(): string { + let mode = ""; + if (this.mode == DamageEffectMode.HULL_ONLY) { + mode = " hull"; + } else if (this.mode == DamageEffectMode.SHIELD_ONLY) { + mode = " shield"; + } else if (this.mode == DamageEffectMode.SHIELD_THEN_HULL) { + mode = " piercing"; + } + + let modifier = ""; + if (!this.evadable) { + modifier = " unevadable"; + } + + return `do ${this.value}${modifier}${mode} damage`; + } } diff --git a/src/core/effects/PinnedEffect.spec.ts b/src/core/effects/PinnedEffect.spec.ts index 7b36c8f..95a813f 100644 --- a/src/core/effects/PinnedEffect.spec.ts +++ b/src/core/effects/PinnedEffect.spec.ts @@ -1,62 +1,69 @@ -module TK.SpaceTac.Specs { - testing("PinnedEffect", test => { - test.case("shows a textual description", check => { - check.equals(new PinnedEffect().getDescription(), "pinned"); - check.equals(new PinnedEffect(true).getDescription(), "anchored"); - }); +import { testing } from "../../common/Testing"; +import { ActionUnavailability } from "../actions/BaseAction"; +import { Ship } from "../Ship"; +import { TestTools } from "../TestTools"; +import { BaseEffect } from "./BaseEffect"; +import { PinnedEffect } from "./PinnedEffect"; +import { RepelEffect } from "./RepelEffect"; +import { StickyEffect } from "./StickyEffect"; - test.case("prevents a ship from using its engine", check => { - let ship = new Ship(); - TestTools.setShipModel(ship, 1, 1, 1); +testing("PinnedEffect", test => { + test.case("shows a textual description", check => { + check.equals(new PinnedEffect().getDescription(), "pinned"); + check.equals(new PinnedEffect(true).getDescription(), "anchored"); + }); - let engine = TestTools.addEngine(ship, 100); - check.equals(engine.checkCannotBeApplied(ship), null, "engine can initially be used"); + test.case("prevents a ship from using its engine", check => { + let ship = new Ship(); + TestTools.setShipModel(ship, 1, 1, 1); - let cases: [string, BaseEffect][] = [ - ["soft pin", new PinnedEffect()], - ["hard pin", new PinnedEffect(true)], - ["sticky soft pin", new StickyEffect(new PinnedEffect())], - ["sticky hard pin", new StickyEffect(new PinnedEffect(true))], - ]; - cases.forEach(([title, effect]) => { - check.in(title, check => { - ship.active_effects.add(effect); - check.equals(engine.checkCannotBeApplied(ship), ActionUnavailability.PINNED, "engine cannot be used when pinned"); - ship.active_effects.remove(effect); - check.equals(engine.checkCannotBeApplied(ship), null, "engine can be used again"); - }); - }); - }); + let engine = TestTools.addEngine(ship, 100); + check.equals(engine.checkCannotBeApplied(ship), null, "engine can initially be used"); - test.case("prevents a ship from being moved by another effect, in hard mode", check => { - let battle = TestTools.createBattle(); - let ship = battle.fleets[0].ships[0]; - let enemy = battle.fleets[1].ships[0]; - enemy.setArenaPosition(0, 500); - - let effect = new RepelEffect(100); - check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can initially be moved"); - - check.in("soft pin", check => { - let pin = enemy.active_effects.add(new PinnedEffect()); - check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can still be moved"); - enemy.active_effects.remove(pin); - check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can still be moved"); - }); - - check.in("hard pin", check => { - let pin = enemy.active_effects.add(new PinnedEffect(true)); - check.equals(effect.getOnDiffs(enemy, ship).length, 0, "ship cannot be moved"); - enemy.active_effects.remove(pin); - check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can be moved again"); - }); - - check.in("sticky hard pin", check => { - let pin = enemy.active_effects.add(new StickyEffect(new PinnedEffect(true))); - check.equals(effect.getOnDiffs(enemy, ship).length, 0, "ship cannot be moved"); - enemy.active_effects.remove(pin); - check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can be moved again"); - }); - }); + let cases: [string, BaseEffect][] = [ + ["soft pin", new PinnedEffect()], + ["hard pin", new PinnedEffect(true)], + ["sticky soft pin", new StickyEffect(new PinnedEffect())], + ["sticky hard pin", new StickyEffect(new PinnedEffect(true))], + ]; + cases.forEach(([title, effect]) => { + check.in(title, check => { + ship.active_effects.add(effect); + check.equals(engine.checkCannotBeApplied(ship), ActionUnavailability.PINNED, "engine cannot be used when pinned"); + ship.active_effects.remove(effect); + check.equals(engine.checkCannotBeApplied(ship), null, "engine can be used again"); + }); }); -} + }); + + test.case("prevents a ship from being moved by another effect, in hard mode", check => { + let battle = TestTools.createBattle(); + let ship = battle.fleets[0].ships[0]; + let enemy = battle.fleets[1].ships[0]; + enemy.setArenaPosition(0, 500); + + let effect = new RepelEffect(100); + check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can initially be moved"); + + check.in("soft pin", check => { + let pin = enemy.active_effects.add(new PinnedEffect()); + check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can still be moved"); + enemy.active_effects.remove(pin); + check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can still be moved"); + }); + + check.in("hard pin", check => { + let pin = enemy.active_effects.add(new PinnedEffect(true)); + check.equals(effect.getOnDiffs(enemy, ship).length, 0, "ship cannot be moved"); + enemy.active_effects.remove(pin); + check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can be moved again"); + }); + + check.in("sticky hard pin", check => { + let pin = enemy.active_effects.add(new StickyEffect(new PinnedEffect(true))); + check.equals(effect.getOnDiffs(enemy, ship).length, 0, "ship cannot be moved"); + enemy.active_effects.remove(pin); + check.equals(effect.getOnDiffs(enemy, ship).length, 1, "ship can be moved again"); + }); + }); +}); diff --git a/src/core/effects/PinnedEffect.ts b/src/core/effects/PinnedEffect.ts index 1be60a8..a919241 100644 --- a/src/core/effects/PinnedEffect.ts +++ b/src/core/effects/PinnedEffect.ts @@ -1,22 +1,20 @@ -/// +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Pin a ship in space, preventing him from moving using its engine - * - * If hard pinned, the ship also may not be moved by another MoveEffect - */ - export class PinnedEffect extends BaseEffect { - constructor(readonly hard = false) { - super("pinned"); - } +/** + * Pin a ship in space, preventing him from moving using its engine + * + * If hard pinned, the ship also may not be moved by another MoveEffect + */ +export class PinnedEffect extends BaseEffect { + constructor(readonly hard = false) { + super("pinned"); + } - isBeneficial(): boolean { - return false; - } + isBeneficial(): boolean { + return false; + } - getDescription(): string { - return this.hard ? "anchored" : "pinned"; - } - } + getDescription(): string { + return this.hard ? "anchored" : "pinned"; + } } diff --git a/src/core/effects/RepelEffect.spec.ts b/src/core/effects/RepelEffect.spec.ts index 5c803bd..ea78645 100644 --- a/src/core/effects/RepelEffect.spec.ts +++ b/src/core/effects/RepelEffect.spec.ts @@ -1,40 +1,43 @@ -module TK.SpaceTac.Specs { - testing("RepelEffect", test => { - test.case("shows a textual description", check => { - check.equals(new RepelEffect(34).getDescription(), "repel ships 34km away"); - }) +import { testing } from "../../common/Testing"; +import { ArenaLocationAngle } from "../ArenaLocation"; +import { Battle } from "../Battle"; +import { RepelEffect } from "./RepelEffect"; - test.case("repel other ships from a central point", check => { - let battle = new Battle(); - let ship1a = battle.fleets[0].addShip(); - ship1a.setArenaPosition(100, 100); - let ship1b = battle.fleets[0].addShip(); - ship1b.setArenaPosition(250, 100); - let ship2a = battle.fleets[1].addShip(); - ship2a.setArenaPosition(100, 280); +testing("RepelEffect", test => { + test.case("shows a textual description", check => { + check.equals(new RepelEffect(34).getDescription(), "repel ships 34km away"); + }) - let effect = new RepelEffect(12); - battle.applyDiffs(effect.getOnDiffs(ship1a, ship1a)); - battle.applyDiffs(effect.getOnDiffs(ship1b, ship1a)); - battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a)); + test.case("repel other ships from a central point", check => { + let battle = new Battle(); + let ship1a = battle.fleets[0].addShip(); + ship1a.setArenaPosition(100, 100); + let ship1b = battle.fleets[0].addShip(); + ship1b.setArenaPosition(250, 100); + let ship2a = battle.fleets[1].addShip(); + ship2a.setArenaPosition(100, 280); - check.equals(ship1a.location, new ArenaLocationAngle(100, 100)); - check.equals(ship1b.location, new ArenaLocationAngle(262, 100)); - check.equals(ship2a.location, new ArenaLocationAngle(100, 292)); - }) + let effect = new RepelEffect(12); + battle.applyDiffs(effect.getOnDiffs(ship1a, ship1a)); + battle.applyDiffs(effect.getOnDiffs(ship1b, ship1a)); + battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a)); - test.case("does not push a ship inside a hard exclusion area", check => { - let battle = new Battle(); - let ship1a = battle.fleets[0].addShip(); - ship1a.setArenaPosition(100, 100); - let ship2a = battle.fleets[1].addShip(); - ship2a.setArenaPosition(100, 200); - let ship2b = battle.fleets[1].addShip(); - ship2b.setArenaPosition(100, 350); + check.equals(ship1a.location, new ArenaLocationAngle(100, 100)); + check.equals(ship1b.location, new ArenaLocationAngle(262, 100)); + check.equals(ship2a.location, new ArenaLocationAngle(100, 292)); + }) - let effect = new RepelEffect(85); - battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a)); - check.equals(ship2a.location, new ArenaLocationAngle(100, 250)); - }) - }) -} + test.case("does not push a ship inside a hard exclusion area", check => { + let battle = new Battle(); + let ship1a = battle.fleets[0].addShip(); + ship1a.setArenaPosition(100, 100); + let ship2a = battle.fleets[1].addShip(); + ship2a.setArenaPosition(100, 200); + let ship2b = battle.fleets[1].addShip(); + ship2b.setArenaPosition(100, 350); + + let effect = new RepelEffect(85); + battle.applyDiffs(effect.getOnDiffs(ship2a, ship1a)); + check.equals(ship2a.location, new ArenaLocationAngle(100, 250)); + }) +}) diff --git a/src/core/effects/RepelEffect.ts b/src/core/effects/RepelEffect.ts index 9c92640..187171d 100644 --- a/src/core/effects/RepelEffect.ts +++ b/src/core/effects/RepelEffect.ts @@ -1,35 +1,41 @@ -/// +import { any } from "../../common/Tools"; +import { arenaAngle, ArenaLocation, ArenaLocationAngle } from "../ArenaLocation"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipMoveDiff } from "../diffs/ShipMoveDiff"; +import { Drone } from "../Drone"; +import { ExclusionAreas } from "../ExclusionAreas"; +import { Ship } from "../Ship"; +import { BaseEffect } from "./BaseEffect"; +import { PinnedEffect } from "./PinnedEffect"; -module TK.SpaceTac { - /** - * Repel ships from a central point - */ - export class RepelEffect extends BaseEffect { - value: number; +/** + * Repel ships from a central point + */ +export class RepelEffect extends BaseEffect { + value: number; - constructor(value = 0) { - super("repel"); + constructor(value = 0) { + super("repel"); - this.value = value; - } + this.value = value; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - if (ship != source && !any(ship.getEffects(), effect => effect instanceof PinnedEffect && effect.hard)) { - let angle = arenaAngle(source.location, ship.location); - let destination = new ArenaLocation(ship.arena_x + Math.cos(angle) * this.value, ship.arena_y + Math.sin(angle) * this.value); - let exclusions = ExclusionAreas.fromShip(ship); - destination = exclusions.stopBefore(destination, ship.location); - // TODO Apply area effect adding/removal - return [ - new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(destination.x, destination.y, ship.arena_angle)) - ]; - } else { - return []; - } - } - - getDescription(): string { - return `repel ships ${this.value}km away`; - } + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + if (ship != source && !any(ship.getEffects(), effect => effect instanceof PinnedEffect && effect.hard)) { + let angle = arenaAngle(source.location, ship.location); + let destination = new ArenaLocation(ship.arena_x + Math.cos(angle) * this.value, ship.arena_y + Math.sin(angle) * this.value); + let exclusions = ExclusionAreas.fromShip(ship); + destination = exclusions.stopBefore(destination, ship.location); + // TODO Apply area effect adding/removal + return [ + new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(destination.x, destination.y, ship.arena_angle)) + ]; + } else { + return []; } + } + + getDescription(): string { + return `repel ships ${this.value}km away`; + } } diff --git a/src/core/effects/StickyEffect.spec.ts b/src/core/effects/StickyEffect.spec.ts index 2facd13..26b0d3b 100644 --- a/src/core/effects/StickyEffect.spec.ts +++ b/src/core/effects/StickyEffect.spec.ts @@ -1,47 +1,51 @@ -module TK.SpaceTac.Specs { - testing("StickyEffect", test => { - test.case("applies to ship", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); +import { testing } from "../../common/Testing"; +import { Battle } from "../Battle"; +import { AttributeEffect } from "./AttributeEffect"; +import { DamageEffect } from "./DamageEffect"; +import { StickyEffect } from "./StickyEffect"; - check.in("before", check => { - check.equals(ship.active_effects.count(), 0, "no sticky effect"); - check.equals(ship.getAttribute("evasion"), 0, "evasion"); - }) +testing("StickyEffect", test => { + test.case("applies to ship", check => { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); - let effect = new StickyEffect(new AttributeEffect("evasion", 1), 2); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.in("before", check => { + check.equals(ship.active_effects.count(), 0, "no sticky effect"); + check.equals(ship.getAttribute("evasion"), 0, "evasion"); + }) - check.in("after", check => { - check.equals(ship.active_effects.count(), 1, "one sticky effect"); - let sticked = ship.active_effects.list()[0]; - if (sticked instanceof StickyEffect) { - check.equals(sticked.base, effect.base, "sticked effect"); - check.equals(sticked.duration, 2, "sticked duration"); - check.equals(ship.getAttribute("evasion"), 1, "evasion"); - sticked.duration = 1; - } else { - check.fail("Not a sticky effect"); - } - }) + let effect = new StickyEffect(new AttributeEffect("evasion", 1), 2); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.in("after", check => { + check.equals(ship.active_effects.count(), 1, "one sticky effect"); + let sticked = ship.active_effects.list()[0]; + if (sticked instanceof StickyEffect) { + check.equals(sticked.base, effect.base, "sticked effect"); + check.equals(sticked.duration, 2, "sticked duration"); + check.equals(ship.getAttribute("evasion"), 1, "evasion"); + sticked.duration = 1; + } else { + check.fail("Not a sticky effect"); + } + }) - check.in("after second apply", check => { - check.equals(ship.active_effects.count(), 1, "one sticky effect"); - let sticked = ship.active_effects.list()[0]; - if (sticked instanceof StickyEffect) { - check.equals(sticked.base, effect.base, "sticked effect"); - check.equals(sticked.duration, 2, "sticked duration"); - check.equals(ship.getAttribute("evasion"), 1, "evasion"); - } else { - check.fail("Not a sticky effect"); - } - }) - }); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); - test.case("gets a textual description", check => { - check.equals(new StickyEffect(new DamageEffect(10), 2).getDescription(), "do 10 damage for 2 turns"); - }); - }); -} + check.in("after second apply", check => { + check.equals(ship.active_effects.count(), 1, "one sticky effect"); + let sticked = ship.active_effects.list()[0]; + if (sticked instanceof StickyEffect) { + check.equals(sticked.base, effect.base, "sticked effect"); + check.equals(sticked.duration, 2, "sticked duration"); + check.equals(ship.getAttribute("evasion"), 1, "evasion"); + } else { + check.fail("Not a sticky effect"); + } + }) + }); + + test.case("gets a textual description", check => { + check.equals(new StickyEffect(new DamageEffect(10), 2).getDescription(), "do 10 damage for 2 turns"); + }); +}); diff --git a/src/core/effects/StickyEffect.ts b/src/core/effects/StickyEffect.ts index 48a1368..a196bed 100644 --- a/src/core/effects/StickyEffect.ts +++ b/src/core/effects/StickyEffect.ts @@ -1,82 +1,85 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "../diffs/ShipEffectAddedDiff"; +import { ShipEffectChangedDiff } from "../diffs/ShipEffectChangedDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Wrapper around another effect, to make it stick to a ship for a given number of turns. - * - * The "effect" is to stick the wrapped effect to the ship. - */ - export class StickyEffect extends BaseEffect { - // Wrapped effect - base: BaseEffect +/** + * Wrapper around another effect, to make it stick to a ship for a given number of turns. + * + * The "effect" is to stick the wrapped effect to the ship. + */ +export class StickyEffect extends BaseEffect { + // Wrapped effect + base: BaseEffect - // Duration, in number of turns - duration: number + // Duration, in number of turns + duration: number - // Base constructor - constructor(base: BaseEffect, duration = 0) { - super(base.code); + // Base constructor + constructor(base: BaseEffect, duration = 0) { + super(base.code); - this.base = base; - this.duration = duration; - } + this.base = base; + this.duration = duration; + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; - let previous = ship.active_effects.get(this.id); - if (previous) { - result = result.concat(previous.getOffDiffs(ship)); - } - - result.push(new ShipEffectAddedDiff(ship, this)); - result = result.concat(this.base.getOnDiffs(ship, source)); - - return result; - } - - getOffDiffs(ship: Ship): BaseBattleDiff[] { - let result: BaseBattleDiff[] = []; - - if (ship.active_effects.get(this.id)) { - result.push(new ShipEffectRemovedDiff(ship, this)); - result = result.concat(this.base.getOffDiffs(ship)); - } - - return result; - } - - getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { - if (ship.active_effects.get(this.id)) { - return this.base.getTurnStartDiffs(ship); - } else { - return []; - } - } - - getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { - if (ship.active_effects.get(this.id)) { - if (this.duration > 1) { - let result: BaseBattleDiff[] = [new ShipEffectChangedDiff(ship, this, -1)]; - return result.concat(this.base.getTurnEndDiffs(ship)); - } else { - return this.getOffDiffs(ship); - } - } else { - return []; - } - } - - isBeneficial(): boolean { - return this.base.isBeneficial(); - } - - getFullCode(): string { - return this.base.getFullCode(); - } - - getDescription(): string { - return this.base.getDescription() + ` for ${this.duration} turn${this.duration > 1 ? "s" : ""}`; - } + let previous = ship.active_effects.get(this.id); + if (previous) { + result = result.concat(previous.getOffDiffs(ship)); } + + result.push(new ShipEffectAddedDiff(ship, this)); + result = result.concat(this.base.getOnDiffs(ship, source)); + + return result; + } + + getOffDiffs(ship: Ship): BaseBattleDiff[] { + let result: BaseBattleDiff[] = []; + + if (ship.active_effects.get(this.id)) { + result.push(new ShipEffectRemovedDiff(ship, this)); + result = result.concat(this.base.getOffDiffs(ship)); + } + + return result; + } + + getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { + if (ship.active_effects.get(this.id)) { + return this.base.getTurnStartDiffs(ship); + } else { + return []; + } + } + + getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { + if (ship.active_effects.get(this.id)) { + if (this.duration > 1) { + let result: BaseBattleDiff[] = [new ShipEffectChangedDiff(ship, this, -1)]; + return result.concat(this.base.getTurnEndDiffs(ship)); + } else { + return this.getOffDiffs(ship); + } + } else { + return []; + } + } + + isBeneficial(): boolean { + return this.base.isBeneficial(); + } + + getFullCode(): string { + return this.base.getFullCode(); + } + + getDescription(): string { + return this.base.getDescription() + ` for ${this.duration} turn${this.duration > 1 ? "s" : ""}`; + } } diff --git a/src/core/effects/ValueEffect.spec.ts b/src/core/effects/ValueEffect.spec.ts index e67c432..cf2711c 100644 --- a/src/core/effects/ValueEffect.spec.ts +++ b/src/core/effects/ValueEffect.spec.ts @@ -1,103 +1,105 @@ -module TK.SpaceTac { - testing("ValueEffect", test => { - test.case("adds an amount to a ship value", check => { - let effect = new ValueEffect("shield", 20); +import { testing } from "../../common/Testing"; +import { Battle } from "../Battle"; +import { ValueEffect } from "./ValueEffect"; - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - ship.setValue("shield", 55); - check.equals(ship.getValue("shield"), 55); +testing("ValueEffect", test => { + test.case("adds an amount to a ship value", check => { + let effect = new ValueEffect("shield", 20); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(ship.getValue("shield"), 75); + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + ship.setValue("shield", 55); + check.equals(ship.getValue("shield"), 55); - battle.applyDiffs(effect.getOnDiffs(ship, ship)); - check.equals(ship.getValue("shield"), 95); - }); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(ship.getValue("shield"), 75); - test.case("estimates if the effect is beneficial", check => { - let effect = new ValueEffect("hull", 12); - check.equals(effect.isBeneficial(), true, "12"); + battle.applyDiffs(effect.getOnDiffs(ship, ship)); + check.equals(ship.getValue("shield"), 95); + }); - effect = new ValueEffect("hull", -12); - check.equals(effect.isBeneficial(), false, "-12"); + test.case("estimates if the effect is beneficial", check => { + let effect = new ValueEffect("hull", 12); + check.equals(effect.isBeneficial(), true, "12"); - effect = new ValueEffect("hull", 0, 8); - check.equals(effect.isBeneficial(), true, "0 8"); + effect = new ValueEffect("hull", -12); + check.equals(effect.isBeneficial(), false, "-12"); - effect = new ValueEffect("hull", 0, -8); - check.equals(effect.isBeneficial(), false, "0 -8"); + effect = new ValueEffect("hull", 0, 8); + check.equals(effect.isBeneficial(), true, "0 8"); - effect = new ValueEffect("hull", 4, -3); - check.equals(effect.isBeneficial(), true, "4 -3"); + effect = new ValueEffect("hull", 0, -8); + check.equals(effect.isBeneficial(), false, "0 -8"); - effect = new ValueEffect("hull", 4, -4); - check.equals(effect.isBeneficial(), true, "4 -4"); + effect = new ValueEffect("hull", 4, -3); + check.equals(effect.isBeneficial(), true, "4 -3"); - effect = new ValueEffect("hull", 3, -4); - check.equals(effect.isBeneficial(), false, "3 -4"); + effect = new ValueEffect("hull", 4, -4); + check.equals(effect.isBeneficial(), true, "4 -4"); - effect = new ValueEffect("hull", -4, 4); - check.equals(effect.isBeneficial(), false, "-4 4"); + effect = new ValueEffect("hull", 3, -4); + check.equals(effect.isBeneficial(), false, "3 -4"); - effect = new ValueEffect("hull", 0, 0, 12); - check.equals(effect.isBeneficial(), true, "0 0 12"); + effect = new ValueEffect("hull", -4, 4); + check.equals(effect.isBeneficial(), false, "-4 4"); - effect = new ValueEffect("hull", 0, 0, -12); - check.equals(effect.isBeneficial(), false, "0 0 -12"); + effect = new ValueEffect("hull", 0, 0, 12); + check.equals(effect.isBeneficial(), true, "0 0 12"); - effect = new ValueEffect("hull", 0, 0, 0, 8); - check.equals(effect.isBeneficial(), true, "0 0 0 8"); + effect = new ValueEffect("hull", 0, 0, -12); + check.equals(effect.isBeneficial(), false, "0 0 -12"); - effect = new ValueEffect("hull", 0, 0, 0, -8); - check.equals(effect.isBeneficial(), false, "0 0 0 -8"); + effect = new ValueEffect("hull", 0, 0, 0, 8); + check.equals(effect.isBeneficial(), true, "0 0 0 8"); - effect = new ValueEffect("hull", 0, 0, 4, -3); - check.equals(effect.isBeneficial(), true, "0 0 4 -3"); + effect = new ValueEffect("hull", 0, 0, 0, -8); + check.equals(effect.isBeneficial(), false, "0 0 0 -8"); - effect = new ValueEffect("hull", 0, 0, 4, -4); - check.equals(effect.isBeneficial(), true, "0 0 4 -4"); + effect = new ValueEffect("hull", 0, 0, 4, -3); + check.equals(effect.isBeneficial(), true, "0 0 4 -3"); - effect = new ValueEffect("hull", 0, 0, 3, -4); - check.equals(effect.isBeneficial(), false, "0 0 3 -4"); + effect = new ValueEffect("hull", 0, 0, 4, -4); + check.equals(effect.isBeneficial(), true, "0 0 4 -4"); - effect = new ValueEffect("hull", 0, 0, -4, 4); - check.equals(effect.isBeneficial(), false, "0 0 -4 4"); - }); + effect = new ValueEffect("hull", 0, 0, 3, -4); + check.equals(effect.isBeneficial(), false, "0 0 3 -4"); - test.case("has a description", check => { - let effect = new ValueEffect("power", 12); - check.equals(effect.getDescription(), "power +12"); + effect = new ValueEffect("hull", 0, 0, -4, 4); + check.equals(effect.isBeneficial(), false, "0 0 -4 4"); + }); - effect = new ValueEffect("power", -4); - check.equals(effect.getDescription(), "power -4"); + test.case("has a description", check => { + let effect = new ValueEffect("power", 12); + check.equals(effect.getDescription(), "power +12"); - effect = new ValueEffect("power"); - check.equals(effect.getDescription(), "no effect"); + effect = new ValueEffect("power", -4); + check.equals(effect.getDescription(), "power -4"); - effect = new ValueEffect("power", 0, -5); - check.equals(effect.getDescription(), "power -5 when removed"); + effect = new ValueEffect("power"); + check.equals(effect.getDescription(), "no effect"); - effect = new ValueEffect("power", 5, -5); - check.equals(effect.getDescription(), "power +5 while active"); + effect = new ValueEffect("power", 0, -5); + check.equals(effect.getDescription(), "power -5 when removed"); - effect = new ValueEffect("power", 5, -6); - check.equals(effect.getDescription(), "power +5 on, -6 off"); + effect = new ValueEffect("power", 5, -5); + check.equals(effect.getDescription(), "power +5 while active"); - effect = new ValueEffect("power", 0, 0, 6); - check.equals(effect.getDescription(), "power +6 on turn start"); + effect = new ValueEffect("power", 5, -6); + check.equals(effect.getDescription(), "power +5 on, -6 off"); - effect = new ValueEffect("power", 0, 0, 0, -3); - check.equals(effect.getDescription(), "power -3 on turn end"); + effect = new ValueEffect("power", 0, 0, 6); + check.equals(effect.getDescription(), "power +6 on turn start"); - effect = new ValueEffect("power", 0, 0, 3, -3); - check.equals(effect.getDescription(), "power +3 during turn"); + effect = new ValueEffect("power", 0, 0, 0, -3); + check.equals(effect.getDescription(), "power -3 on turn end"); - effect = new ValueEffect("power", 0, 0, 4, -3); - check.equals(effect.getDescription(), "power +4 on turn start, -3 on turn end"); + effect = new ValueEffect("power", 0, 0, 3, -3); + check.equals(effect.getDescription(), "power +3 during turn"); - effect = new ValueEffect("power", 1, 2, 3, 4); - check.equals(effect.getDescription(), "power +1 on, +2 off, +3 on turn start, +4 on turn end"); - }); - }); -} + effect = new ValueEffect("power", 0, 0, 4, -3); + check.equals(effect.getDescription(), "power +4 on turn start, -3 on turn end"); + + effect = new ValueEffect("power", 1, 2, 3, 4); + check.equals(effect.getDescription(), "power +1 on, +2 off, +3 on turn start, +4 on turn end"); + }); +}); diff --git a/src/core/effects/ValueEffect.ts b/src/core/effects/ValueEffect.ts index b5eaf99..1a17352 100644 --- a/src/core/effects/ValueEffect.ts +++ b/src/core/effects/ValueEffect.ts @@ -1,128 +1,130 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { ShipValues, SHIP_VALUES_NAMES } from "../ShipValue"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - function strval(value: number) { - return `${value > 0 ? "+" : "-"}${Math.abs(value)}`; - } - - /** - * Effect to add (or subtract if negative) an amount to a ship value. - * - * The effect is immediate and permanent. - */ - export class ValueEffect extends BaseEffect { - // Affected value - valuetype: keyof ShipValues - - // Value to add (or subtract if negative), when the effect is applied to a ship - value_on: number - - // Value to add (or subtract if negative), when the effect is removed from a ship - value_off: number - - // Value to add (or subtract if negative), when the effect is active on a ship starting its turn - value_start: number - - // Value to add (or subtract if negative), when the effect is active on a ship ending its turn - value_end: number - - constructor(valuetype: keyof ShipValues, value_on = 0, value_off = 0, value_start = 0, value_end = 0) { - super("value"); - - this.valuetype = valuetype; - this.value_on = value_on; - this.value_off = value_off; - this.value_start = value_start; - this.value_end = value_end; - } - - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - if (this.value_on) { - return ship.getValueDiffs(this.valuetype, this.value_on, true); - } else { - return []; - } - } - - getOffDiffs(ship: Ship): BaseBattleDiff[] { - if (this.value_off) { - return ship.getValueDiffs(this.valuetype, this.value_off, true); - } else { - return []; - } - } - - getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { - if (this.value_start) { - return ship.getValueDiffs(this.valuetype, this.value_start, true); - } else { - return []; - } - } - - getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { - if (this.value_end) { - return ship.getValueDiffs(this.valuetype, this.value_end, true); - } else { - return []; - } - } - - isBeneficial(): boolean { - if (this.value_off < -this.value_on || this.value_end < -this.value_start) { - // after value is lower than before - return false; - } else if ((this.value_off && this.value_off == -this.value_on) || (this.value_end && this.value_end == -this.value_start)) { - return this.value_on > 0 || this.value_start > 0; - } else { - return this.value_on > 0 || this.value_off > 0 || this.value_start > 0 || this.value_end > 0; - } - } - - getFullCode(): string { - return `${this.code}-${this.valuetype}`; - } - - getDescription(): string { - let attrname = SHIP_VALUES_NAMES[this.valuetype]; - - let parts: string[] = []; - - if (this.value_on) { - if (this.value_off == -this.value_on) { - parts.push(`${strval(this.value_on)} while active`); - } else { - if (this.value_off) { - parts.push(`${strval(this.value_on)} on`); - parts.push(`${strval(this.value_off)} off`); - } else { - parts.push(strval(this.value_on)); - } - } - } - - if (this.value_start) { - if (this.value_end == -this.value_start) { - parts.push(`${strval(this.value_start)} during turn`); - } else { - parts.push(`${strval(this.value_start)} on turn start`); - if (this.value_end) { - parts.push(`${strval(this.value_end)} on turn end`); - } - } - } else if (this.value_end) { - parts.push(`${strval(this.value_end)} on turn end`); - } - - if (this.value_off && !this.value_on) { - parts.push(`${strval(this.value_off)} when removed`); - } - - if (parts.length) { - return `${attrname} ${parts.join(', ')}`; - } else { - return "no effect"; - } - } - } +function strval(value: number) { + return `${value > 0 ? "+" : "-"}${Math.abs(value)}`; +} + +/** + * Effect to add (or subtract if negative) an amount to a ship value. + * + * The effect is immediate and permanent. + */ +export class ValueEffect extends BaseEffect { + // Affected value + valuetype: keyof ShipValues + + // Value to add (or subtract if negative), when the effect is applied to a ship + value_on: number + + // Value to add (or subtract if negative), when the effect is removed from a ship + value_off: number + + // Value to add (or subtract if negative), when the effect is active on a ship starting its turn + value_start: number + + // Value to add (or subtract if negative), when the effect is active on a ship ending its turn + value_end: number + + constructor(valuetype: keyof ShipValues, value_on = 0, value_off = 0, value_start = 0, value_end = 0) { + super("value"); + + this.valuetype = valuetype; + this.value_on = value_on; + this.value_off = value_off; + this.value_start = value_start; + this.value_end = value_end; + } + + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + if (this.value_on) { + return ship.getValueDiffs(this.valuetype, this.value_on, true); + } else { + return []; + } + } + + getOffDiffs(ship: Ship): BaseBattleDiff[] { + if (this.value_off) { + return ship.getValueDiffs(this.valuetype, this.value_off, true); + } else { + return []; + } + } + + getTurnStartDiffs(ship: Ship): BaseBattleDiff[] { + if (this.value_start) { + return ship.getValueDiffs(this.valuetype, this.value_start, true); + } else { + return []; + } + } + + getTurnEndDiffs(ship: Ship): BaseBattleDiff[] { + if (this.value_end) { + return ship.getValueDiffs(this.valuetype, this.value_end, true); + } else { + return []; + } + } + + isBeneficial(): boolean { + if (this.value_off < -this.value_on || this.value_end < -this.value_start) { + // after value is lower than before + return false; + } else if ((this.value_off && this.value_off == -this.value_on) || (this.value_end && this.value_end == -this.value_start)) { + return this.value_on > 0 || this.value_start > 0; + } else { + return this.value_on > 0 || this.value_off > 0 || this.value_start > 0 || this.value_end > 0; + } + } + + getFullCode(): string { + return `${this.code}-${this.valuetype}`; + } + + getDescription(): string { + let attrname = SHIP_VALUES_NAMES[this.valuetype]; + + let parts: string[] = []; + + if (this.value_on) { + if (this.value_off == -this.value_on) { + parts.push(`${strval(this.value_on)} while active`); + } else { + if (this.value_off) { + parts.push(`${strval(this.value_on)} on`); + parts.push(`${strval(this.value_off)} off`); + } else { + parts.push(strval(this.value_on)); + } + } + } + + if (this.value_start) { + if (this.value_end == -this.value_start) { + parts.push(`${strval(this.value_start)} during turn`); + } else { + parts.push(`${strval(this.value_start)} on turn start`); + if (this.value_end) { + parts.push(`${strval(this.value_end)} on turn end`); + } + } + } else if (this.value_end) { + parts.push(`${strval(this.value_end)} on turn end`); + } + + if (this.value_off && !this.value_on) { + parts.push(`${strval(this.value_off)} when removed`); + } + + if (parts.length) { + return `${attrname} ${parts.join(', ')}`; + } else { + return "no effect"; + } + } } diff --git a/src/core/effects/ValueTransferEffect.spec.ts b/src/core/effects/ValueTransferEffect.spec.ts index b0361e6..84a1e3c 100644 --- a/src/core/effects/ValueTransferEffect.spec.ts +++ b/src/core/effects/ValueTransferEffect.spec.ts @@ -1,32 +1,35 @@ -module TK.SpaceTac.Specs { - testing("ValueTransferEffect", test => { - test.case("takes or gives value", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - TestTools.setShipModel(ship1, 100, 50); - ship1.setValue("hull", 10); - let ship2 = battle.fleets[0].addShip(); - TestTools.setShipModel(ship2, 100, 50); +import { testing } from "../../common/Testing"; +import { Battle } from "../Battle"; +import { TestTools } from "../TestTools"; +import { ValueTransferEffect } from "./ValueTransferEffect"; - let effect = new ValueTransferEffect("hull", -30); - battle.applyDiffs(effect.getOnDiffs(ship2, ship1)); - check.equals(ship1.getValue("hull"), 40); - check.equals(ship2.getValue("hull"), 70); +testing("ValueTransferEffect", test => { + test.case("takes or gives value", check => { + let battle = new Battle(); + let ship1 = battle.fleets[0].addShip(); + TestTools.setShipModel(ship1, 100, 50); + ship1.setValue("hull", 10); + let ship2 = battle.fleets[0].addShip(); + TestTools.setShipModel(ship2, 100, 50); - effect = new ValueTransferEffect("hull", 1000); - battle.applyDiffs(effect.getOnDiffs(ship2, ship1)); - check.equals(ship1.getValue("hull"), 0); - check.equals(ship2.getValue("hull"), 110); // over limit but will be fixed later - }) + let effect = new ValueTransferEffect("hull", -30); + battle.applyDiffs(effect.getOnDiffs(ship2, ship1)); + check.equals(ship1.getValue("hull"), 40); + check.equals(ship2.getValue("hull"), 70); - test.case("builds a description", check => { - let effect = new ValueTransferEffect("power", 12); - check.equals(effect.getDescription(), "give 12 power"); - check.equals(effect.isBeneficial(), true); + effect = new ValueTransferEffect("hull", 1000); + battle.applyDiffs(effect.getOnDiffs(ship2, ship1)); + check.equals(ship1.getValue("hull"), 0); + check.equals(ship2.getValue("hull"), 110); // over limit but will be fixed later + }) - effect = new ValueTransferEffect("shield", -20); - check.equals(effect.getDescription(), "steal 20 shield"); - check.equals(effect.isBeneficial(), false); - }) - }) -} + test.case("builds a description", check => { + let effect = new ValueTransferEffect("power", 12); + check.equals(effect.getDescription(), "give 12 power"); + check.equals(effect.isBeneficial(), true); + + effect = new ValueTransferEffect("shield", -20); + check.equals(effect.getDescription(), "steal 20 shield"); + check.equals(effect.isBeneficial(), false); + }) +}) diff --git a/src/core/effects/ValueTransferEffect.ts b/src/core/effects/ValueTransferEffect.ts index be014ba..7e57394 100644 --- a/src/core/effects/ValueTransferEffect.ts +++ b/src/core/effects/ValueTransferEffect.ts @@ -1,52 +1,54 @@ -/// +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { ShipValues, SHIP_VALUES_NAMES } from "../ShipValue"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Transfer a value between two ships. - */ - export class ValueTransferEffect extends BaseEffect { - // Affected value - valuetype: keyof ShipValues +/** + * Transfer a value between two ships. + */ +export class ValueTransferEffect extends BaseEffect { + // Affected value + valuetype: keyof ShipValues - // Value to give to target (negative to take it) - amount: number + // Value to give to target (negative to take it) + amount: number - constructor(valuetype: keyof ShipValues, amount = 0) { - super("valuetransfer"); + constructor(valuetype: keyof ShipValues, amount = 0) { + super("valuetransfer"); - this.valuetype = valuetype; - this.amount = amount; - } - - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - if (source instanceof Ship) { - if (this.amount < 0) { - return new ValueTransferEffect(this.valuetype, -this.amount).getOnDiffs(source, ship); - } else { - let amount = Math.min(source.getValue(this.valuetype), this.amount); - if (amount) { - return source.getValueDiffs(this.valuetype, -amount, true).concat(ship.getValueDiffs(this.valuetype, amount, true)); - } else { - return []; - } - } - } else { - return []; - } - } - - isBeneficial(): boolean { - return this.amount >= 0; - } - - getFullCode(): string { - return `${this.code}-${this.valuetype}`; - } - - getDescription(): string { - let attrname = SHIP_VALUES_NAMES[this.valuetype]; - let verb = (this.amount < 0 ? "steal" : "give"); - return `${verb} ${Math.abs(this.amount)} ${attrname}`; + this.valuetype = valuetype; + this.amount = amount; + } + + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + if (source instanceof Ship) { + if (this.amount < 0) { + return new ValueTransferEffect(this.valuetype, -this.amount).getOnDiffs(source, ship); + } else { + let amount = Math.min(source.getValue(this.valuetype), this.amount); + if (amount) { + return source.getValueDiffs(this.valuetype, -amount, true).concat(ship.getValueDiffs(this.valuetype, amount, true)); + } else { + return []; } + } + } else { + return []; } + } + + isBeneficial(): boolean { + return this.amount >= 0; + } + + getFullCode(): string { + return `${this.code}-${this.valuetype}`; + } + + getDescription(): string { + let attrname = SHIP_VALUES_NAMES[this.valuetype]; + let verb = (this.amount < 0 ? "steal" : "give"); + return `${verb} ${Math.abs(this.amount)} ${attrname}`; + } } diff --git a/src/core/effects/VigilanceEffect.spec.ts b/src/core/effects/VigilanceEffect.spec.ts index b64d4b3..b8b7d20 100644 --- a/src/core/effects/VigilanceEffect.spec.ts +++ b/src/core/effects/VigilanceEffect.spec.ts @@ -1,25 +1,33 @@ -module TK.SpaceTac.Specs { - testing("VigilanceEffect", test => { - test.case("applies vigilance effects on intruding ships", check => { - let battle = new Battle(); - let source = battle.fleets[0].addShip(); - let target = battle.fleets[1].addShip(); +import { testing } from "../../common/Testing"; +import { VigilanceAction } from "../actions/VigilanceAction"; +import { Battle } from "../Battle"; +import { ShipDamageDiff } from "../diffs/ShipDamageDiff"; +import { ShipValueDiff } from "../diffs/ShipValueDiff"; +import { VigilanceAppliedDiff } from "../diffs/VigilanceAppliedDiff"; +import { TestTools } from "../TestTools"; +import { DamageEffect } from "./DamageEffect"; +import { VigilanceEffect } from "./VigilanceEffect"; - let action = source.actions.addCustom(new VigilanceAction("Reactive Shot")); - action.configureVigilance({ intruder_effects: [new DamageEffect(1)] }); - let effect = new VigilanceEffect(action); +testing("VigilanceEffect", test => { + test.case("applies vigilance effects on intruding ships", check => { + let battle = new Battle(); + let source = battle.fleets[0].addShip(); + let target = battle.fleets[1].addShip(); - let diffs = effect.getOnDiffs(target, source); - check.equals(diffs, []); + let action = source.actions.addCustom(new VigilanceAction("Reactive Shot")); + action.configureVigilance({ intruder_effects: [new DamageEffect(1)] }); + let effect = new VigilanceEffect(action); - TestTools.setShipModel(target, 10); + let diffs = effect.getOnDiffs(target, source); + check.equals(diffs, []); - diffs = effect.getOnDiffs(target, source); - check.equals(diffs, [ - new VigilanceAppliedDiff(source, action, target), - new ShipDamageDiff(target, 1, 0), - new ShipValueDiff(target, "hull", -1) - ]); - }) - }) -} + TestTools.setShipModel(target, 10); + + diffs = effect.getOnDiffs(target, source); + check.equals(diffs, [ + new VigilanceAppliedDiff(source, action, target), + new ShipDamageDiff(target, 1, 0), + new ShipValueDiff(target, "hull", -1) + ]); + }) +}) diff --git a/src/core/effects/VigilanceEffect.ts b/src/core/effects/VigilanceEffect.ts index a3c57fe..f122444 100644 --- a/src/core/effects/VigilanceEffect.ts +++ b/src/core/effects/VigilanceEffect.ts @@ -1,36 +1,40 @@ -/// +import { flatten } from "../../common/Tools"; +import { VigilanceAction } from "../actions/VigilanceAction"; +import { BaseBattleDiff } from "../diffs/BaseBattleDiff"; +import { VigilanceAppliedDiff } from "../diffs/VigilanceAppliedDiff"; +import { Drone } from "../Drone"; +import { Ship } from "../Ship"; +import { BaseEffect } from "./BaseEffect"; -module TK.SpaceTac { - /** - * Apply vigilance effects on a ship that enters a vigilance area - */ - export class VigilanceEffect extends BaseEffect { - constructor(private action: VigilanceAction) { - super("vigilance"); - } +/** + * Apply vigilance effects on a ship that enters a vigilance area + */ +export class VigilanceEffect extends BaseEffect { + constructor(private action: VigilanceAction) { + super("vigilance"); + } - getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { - if (source instanceof Ship) { - let result = flatten(this.action.intruder_effects.map(effect => effect.getOnDiffs(ship, source))); - if (result.length > 0) { - result.unshift(new VigilanceAppliedDiff(source, this.action, ship)); - } - return result; - } else { - return []; - } - } - - isInternal(): boolean { - return true; - } - - isBeneficial(): boolean { - return false; - } - - getDescription(): string { - return `Vigilance from ${this.action.name}`; - } + getOnDiffs(ship: Ship, source: Ship | Drone): BaseBattleDiff[] { + if (source instanceof Ship) { + let result = flatten(this.action.intruder_effects.map(effect => effect.getOnDiffs(ship, source))); + if (result.length > 0) { + result.unshift(new VigilanceAppliedDiff(source, this.action, ship)); + } + return result; + } else { + return []; } + } + + isInternal(): boolean { + return true; + } + + isBeneficial(): boolean { + return false; + } + + getDescription(): string { + return `Vigilance from ${this.action.name}`; + } } diff --git a/src/core/missions/ActiveMissions.spec.ts b/src/core/missions/ActiveMissions.spec.ts index e17a262..83f2439 100644 --- a/src/core/missions/ActiveMissions.spec.ts +++ b/src/core/missions/ActiveMissions.spec.ts @@ -1,99 +1,108 @@ -module TK.SpaceTac.Specs { - testing("ActiveMissions", test => { - test.case("starts the main story arc", check => { - let missions = new ActiveMissions(); - check.equals(missions.main, null); +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { Fleet } from "../Fleet"; +import { GameSession } from "../GameSession"; +import { Ship } from "../Ship"; +import { Universe } from "../Universe"; +import { ActiveMissions } from "./ActiveMissions"; +import { Mission } from "./Mission"; +import { MissionPart } from "./MissionPart"; +import { MissionPartConversation } from "./MissionPartConversation"; - let session = new GameSession(); - session.startNewGame(true, false); +testing("ActiveMissions", test => { + test.case("starts the main story arc", check => { + let missions = new ActiveMissions(); + check.equals(missions.main, null); - missions.startMainStory(session.universe, session.player.fleet); - check.notequals(missions.main, null); - }) + let session = new GameSession(); + session.startNewGame(true, false); - test.case("gets the current list of missions, and updates them", check => { - let missions = new ActiveMissions(); - let universe = new Universe(); - let fleet = new Fleet(); + missions.startMainStory(session.universe, session.player.fleet); + check.notequals(missions.main, null); + }) - missions.main = new Mission(universe, fleet); - missions.main.addPart(new MissionPart(missions.main, "Do something")); - missions.secondary = [ - new Mission(universe, fleet), - new Mission(universe, fleet) - ]; - missions.secondary[0].addPart(new MissionPart(missions.secondary[0], "Maybe do something")); - missions.secondary[1].addPart(new MissionPart(missions.secondary[1], "Surely do something")); + test.case("gets the current list of missions, and updates them", check => { + let missions = new ActiveMissions(); + let universe = new Universe(); + let fleet = new Fleet(); - check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ - "Do something", - "Maybe do something", - "Surely do something", - ]); + missions.main = new Mission(universe, fleet); + missions.main.addPart(new MissionPart(missions.main, "Do something")); + missions.secondary = [ + new Mission(universe, fleet), + new Mission(universe, fleet) + ]; + missions.secondary[0].addPart(new MissionPart(missions.secondary[0], "Maybe do something")); + missions.secondary[1].addPart(new MissionPart(missions.secondary[1], "Surely do something")); - missions.checkStatus(); + check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ + "Do something", + "Maybe do something", + "Surely do something", + ]); - check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ - "Do something", - "Maybe do something", - "Surely do something", - ]); + missions.checkStatus(); - check.patch(missions.secondary[0].current_part, "checkCompleted", () => true); - missions.checkStatus(); + check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ + "Do something", + "Maybe do something", + "Surely do something", + ]); - check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ - "Do something", - "Surely do something", - ]); + check.patch(missions.secondary[0].current_part, "checkCompleted", () => true); + missions.checkStatus(); - check.patch(missions.main.current_part, "checkCompleted", () => true); - missions.checkStatus(); + check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ + "Do something", + "Surely do something", + ]); - check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ - "Surely do something", - ]); - check.equals(missions.main, null); - }) + check.patch(missions.main.current_part, "checkCompleted", () => true); + missions.checkStatus(); - test.case("builds a hash to help monitor status changes", check => { - let universe = new Universe(); - universe.generate(4); - let fleet = new Fleet(); - fleet.setLocation(universe.getStartLocation()); + check.equals(missions.getCurrent().map(mission => mission.current_part.title), [ + "Surely do something", + ]); + check.equals(missions.main, null); + }) - let missions = new ActiveMissions(); - let hash = missions.getHash(); - function checkChanged(info: string, expected = true) { - let new_hash = missions.getHash(); - check.same(new_hash != hash, expected, info); - hash = new_hash; - check.equals(missions.getHash(), hash, "Stable after " + info); - } - checkChanged("Stable at init", false); + test.case("builds a hash to help monitor status changes", check => { + let universe = new Universe(); + universe.generate(4); + let fleet = new Fleet(); + fleet.setLocation(universe.getStartLocation()); - missions.startMainStory(universe, fleet); - checkChanged("Main story started"); + let missions = new ActiveMissions(); + let hash = missions.getHash(); + function checkChanged(info: string, expected = true) { + let new_hash = missions.getHash(); + check.same(new_hash != hash, expected, info); + hash = new_hash; + check.equals(missions.getHash(), hash, "Stable after " + info); + } + checkChanged("Stable at init", false); - let mission = new Mission(universe, fleet); - mission.addPart(new MissionPartConversation(mission, [new Ship()])); - mission.addPart(new MissionPartConversation(mission, [new Ship()])); - missions.addSecondary(mission, fleet); - checkChanged("Secondary mission accepted"); + missions.startMainStory(universe, fleet); + checkChanged("Main story started"); - check.equals(mission.getIndex(), 0); - missions.checkStatus(); - check.equals(mission.getIndex(), 1); - checkChanged("First conversation ended"); + let mission = new Mission(universe, fleet); + mission.addPart(new MissionPartConversation(mission, [new Ship()])); + mission.addPart(new MissionPartConversation(mission, [new Ship()])); + missions.addSecondary(mission, fleet); + checkChanged("Secondary mission accepted"); - check.equals(missions.secondary.length, 1); - missions.checkStatus(); - check.equals(missions.secondary.length, 0); - checkChanged("Second conversation ended - mission removed"); + check.equals(mission.getIndex(), 0); + missions.checkStatus(); + check.equals(mission.getIndex(), 1); + checkChanged("First conversation ended"); - nn(missions.main).current_part.forceComplete(); - missions.checkStatus(); - checkChanged("Main mission progress"); - }); - }) -} + check.equals(missions.secondary.length, 1); + missions.checkStatus(); + check.equals(missions.secondary.length, 0); + checkChanged("Second conversation ended - mission removed"); + + nn(missions.main).current_part.forceComplete(); + missions.checkStatus(); + checkChanged("Main mission progress"); + }); +}) diff --git a/src/core/missions/ActiveMissions.ts b/src/core/missions/ActiveMissions.ts index 212f25d..23f5bc4 100644 --- a/src/core/missions/ActiveMissions.ts +++ b/src/core/missions/ActiveMissions.ts @@ -1,66 +1,70 @@ -module TK.SpaceTac { - /** - * A list of active missions - */ - export class ActiveMissions { - main: Mission | null = null - secondary: Mission[] = [] - nextid = 2 +import { sum } from "../../common/Tools"; +import { Fleet } from "../Fleet"; +import { Universe } from "../Universe"; +import { MainStory } from "./MainStory"; +import { Mission } from "./Mission"; - /** - * Start the main story arc - */ - startMainStory(universe: Universe, fleet: Fleet) { - this.main = new MainStory(universe, fleet); - this.main.setStarted(1); - } +/** + * A list of active missions + */ +export class ActiveMissions { + main: Mission | null = null + secondary: Mission[] = [] + nextid = 2 - /** - * Add a secondary mission to the pool - * - * Returns true on success - */ - addSecondary(mission: Mission, fleet: Fleet): boolean { - if (!mission.main && this.secondary.length < 2) { - mission.fleet = fleet; - this.secondary.push(mission); - mission.setStarted(this.nextid++); - return true; - } else { - return false; - } - } + /** + * Start the main story arc + */ + startMainStory(universe: Universe, fleet: Fleet) { + this.main = new MainStory(universe, fleet); + this.main.setStarted(1); + } - /** - * Get the current list of active missions - */ - getCurrent(): Mission[] { - let result: Mission[] = []; - if (this.main) { - result.push(this.main); - } - return result.concat(this.secondary); - } - - /** - * Check status for all active missions - * - * This will remove ended missions - */ - checkStatus(): void { - if (this.main) { - if (!this.main.checkStatus()) { - this.main = null; - } - } - this.secondary = this.secondary.filter(mission => mission.checkStatus()); - } - - /** - * Get a hash that will change when any active mission changes status - */ - getHash(): number { - return sum(this.getCurrent().map(mission => mission.id * 10000 + mission.getIndex())); - } + /** + * Add a secondary mission to the pool + * + * Returns true on success + */ + addSecondary(mission: Mission, fleet: Fleet): boolean { + if (!mission.main && this.secondary.length < 2) { + mission.fleet = fleet; + this.secondary.push(mission); + mission.setStarted(this.nextid++); + return true; + } else { + return false; } + } + + /** + * Get the current list of active missions + */ + getCurrent(): Mission[] { + let result: Mission[] = []; + if (this.main) { + result.push(this.main); + } + return result.concat(this.secondary); + } + + /** + * Check status for all active missions + * + * This will remove ended missions + */ + checkStatus(): void { + if (this.main) { + if (!this.main.checkStatus()) { + this.main = null; + } + } + this.secondary = this.secondary.filter(mission => mission.checkStatus()); + } + + /** + * Get a hash that will change when any active mission changes status + */ + getHash(): number { + return sum(this.getCurrent().map(mission => mission.id * 10000 + mission.getIndex())); + } } diff --git a/src/core/missions/MainStory.spec.ts b/src/core/missions/MainStory.spec.ts index 2652243..08ab282 100644 --- a/src/core/missions/MainStory.spec.ts +++ b/src/core/missions/MainStory.spec.ts @@ -1,64 +1,71 @@ -module TK.SpaceTac.Specs { - testing("MainStory", test => { - function checkPart(story: Mission, index: number, title: RegExp, completed = false) { - let result = story.checkStatus(); - test.check.in(`part ${index}`, check => { - check.same(story.parts.indexOf(story.current_part), index); - check.regex(title, story.current_part.title); - check.same(story.completed, completed); - check.same(result, !completed); - }); - } +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { GameSession } from "../GameSession"; +import { StarLocation } from "../StarLocation"; +import { Mission } from "./Mission"; +import { MissionPartConversation } from "./MissionPartConversation"; +import { MissionPartEscort } from "./MissionPartEscort"; +import { MissionPartGoTo } from "./MissionPartGoTo"; - function goTo(session: GameSession, location: StarLocation, win_encounter = true) { - session.fleet.setLocation(location); +testing("MainStory", test => { + function checkPart(story: Mission, index: number, title: RegExp, completed = false) { + let result = story.checkStatus(); + test.check.in(`part ${index}`, check => { + check.same(story.parts.indexOf(story.current_part), index); + check.regex(title, story.current_part.title); + check.same(story.completed, completed); + check.same(result, !completed); + }); + } - let battle = session.getBattle(); - if (battle) { - battle.endBattle(win_encounter ? session.fleet : battle.fleets[1]); - if (win_encounter) { - session.exitBattle(); - location.clearEncounter(); - } else { - session.revertBattle(); - } - } - } + function goTo(session: GameSession, location: StarLocation, win_encounter = true) { + session.fleet.setLocation(location); - test.case("can be completed", check => { - let session = new GameSession(); - session.startNewGame(true, true); - let fleet = nn(session.player.fleet); + let battle = session.getBattle(); + if (battle) { + battle.endBattle(win_encounter ? session.fleet : battle.fleets[1]); + if (win_encounter) { + session.exitBattle(); + location.clearEncounter(); + } else { + session.revertBattle(); + } + } + } - let missions = session.player.missions; - let story = nn(missions.main); - let fleet_size = fleet.ships.length; + test.case("can be completed", check => { + let session = new GameSession(); + session.startNewGame(true, true); + let fleet = nn(session.player.fleet); - checkPart(story, 0, /^Travel to Terranax galaxy$/); - (story.current_part).skip(); + let missions = session.player.missions; + let story = nn(missions.main); + let fleet_size = fleet.ships.length; - checkPart(story, 1, /^Find your contact in .*$/); - goTo(session, (story.current_part).destination); + checkPart(story, 0, /^Travel to Terranax galaxy$/); + (story.current_part).skip(); - checkPart(story, 2, /^Speak with your contact/); - (story.current_part).skip(); + checkPart(story, 1, /^Find your contact in .*$/); + goTo(session, (story.current_part).destination); - checkPart(story, 3, /^Go with .* in .* system$/); - check.same(fleet.ships.length, fleet_size + 1); - check.same(fleet.ships[fleet_size].critical, true); - check.greater(fleet.ships[fleet_size].getAttribute("hull_capacity"), 0); - goTo(session, (story.current_part).destination); + checkPart(story, 2, /^Speak with your contact/); + (story.current_part).skip(); - checkPart(story, 4, /^Listen to .*$/); - (story.current_part).skip(); - check.equals(session.getBattle(), null); + checkPart(story, 3, /^Go with .* in .* system$/); + check.same(fleet.ships.length, fleet_size + 1); + check.same(fleet.ships[fleet_size].critical, true); + check.greater(fleet.ships[fleet_size].getAttribute("hull_capacity"), 0); + goTo(session, (story.current_part).destination); - checkPart(story, 5, /^Fight the arrived fleet$/); - check.notequals(session.getBattle(), null); - nn(session.getBattle()).endBattle(fleet); - session.setBattleEnded(); + checkPart(story, 4, /^Listen to .*$/); + (story.current_part).skip(); + check.equals(session.getBattle(), null); - check.same(story.checkStatus(), false, "story not complete"); - }) - }) -} + checkPart(story, 5, /^Fight the arrived fleet$/); + check.notequals(session.getBattle(), null); + nn(session.getBattle()).endBattle(fleet); + session.setBattleEnded(); + + check.same(story.checkStatus(), false, "story not complete"); + }) +}) diff --git a/src/core/missions/MainStory.ts b/src/core/missions/MainStory.ts index b768a79..b3c0646 100644 --- a/src/core/missions/MainStory.ts +++ b/src/core/missions/MainStory.ts @@ -1,65 +1,74 @@ -/// +import { RandomGenerator } from "../../common/RandomGenerator"; +import { contains, minBy, nn, nna } from "../../common/Tools"; +import { Fleet } from "../Fleet"; +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionGenerator } from "./MissionGenerator"; +import { MissionPartCleanLocation } from "./MissionPartCleanLocation"; +import { MissionPartConversation } from "./MissionPartConversation"; +import { MissionPartEscort } from "./MissionPartEscort"; +import { MissionPartDestinationHint, MissionPartGoTo } from "./MissionPartGoTo"; -module TK.SpaceTac { - function randomLocation(random: RandomGenerator, stars: Star[], excludes: StarLocation[] = []) { - let star = stars.length == 1 ? stars[0] : random.choice(stars); - return RandomGenerator.global.choice(star.locations.filter(loc => !contains(excludes, loc))); - } - - /** - * Main story arc - */ - export class MainStory extends Mission { - constructor(universe: Universe, fleet: Fleet) { - super(universe, fleet, true); - - let random = RandomGenerator.global; - let start_location = nn(universe.getLocation(fleet.location)); - let mission_generator = new MissionGenerator(universe, start_location); - - // Arrival - let conversation = this.addPart(new MissionPartConversation(this, [], "Travel to Terranax galaxy")); - conversation.addPiece(null, "Wow ! From what my sensors tell me, there is not much activity around here."); - conversation.addPiece(null, "I remember the last time I came in this galaxy, you needed to be aware of collisions at all time, so crowded it was."); - conversation.addPiece(null, "Well...I did not pick a signal from our contact yet. We should be looking for her in this system."); - - // Get in touch with our contact - let contact_location = randomLocation(random, [start_location.star], [start_location]); - let contact_character = mission_generator.generateShip(1); - contact_character.fleet.setLocation(contact_location); - this.addPart(new MissionPartGoTo(this, contact_location, `Find your contact in ${contact_location.star.name}`, MissionPartDestinationHint.SYSTEM)); - conversation = this.addPart(new MissionPartConversation(this, [contact_character], "Speak with your contact")); - conversation.addPiece(contact_character, "Finally, you came!"); - conversation.addPiece(contact_character, "Sorry for not broadcasting my position. As you may have encountered, this star system is not safe anymore."); - conversation.addPiece(null, "Nothing we could not handle, we just hope the other teams have not run across more trouble."); - conversation.addPiece(contact_character, "I do not even know if the other contacts made it to their rendezvous point. Jumping between systems has become quite a hassle around here."); - conversation.addPiece(null, "And we still do not know why those rogue fleets are trying to lockdown the whole galaxy? Did you have some interaction with them?"); - conversation.addPiece(contact_character, "Well, they tend to shoot you on sight if you go near a location they defend. Do not know if that qualifies as interaction though..."); - conversation.addPiece(null, "So where do we go from here? In your last message, you told us of a resistance group growing."); - conversation.addPiece(contact_character, "Yes, some merchants and miners have rallied behind a retired TSF general, but I lost contact with them weeks ago."); - conversation.addPiece(contact_character, "We may go to their last known location, but first I want you to see something in a nearby system."); - conversation.addPiece(contact_character, "If you need any equipment before going, there is a dockyard in this system. They often offer some missions if you need money too."); - conversation.addPiece(null, "Yes, we came here with basic equipment as to not raise suspicion, but it looks like we will need more to defend ourselves."); - conversation.addPiece(null, "Ok, let's go..."); - - // Go take a look at the graveyard - let nearby_systems = nna(start_location.star.getLinks().map(link => link.getPeer(contact_location.star))); - let graveyard_location = randomLocation(random, [minBy(nearby_systems, system => system.level)]); - this.addPart(new MissionPartEscort(this, graveyard_location, contact_character, `Go with ${contact_character.name} in ${graveyard_location.star.name} system`)); - conversation = this.addPart(new MissionPartConversation(this, [contact_character], `Listen to ${contact_character.name}`)); - conversation.addPiece(null, "What is all this junk?"); - conversation.addPiece(contact_character, "This was until recently an unofficial ship scrap yard. Lots of abandoned ship carcasses were floating in here, and many smugglers used to camouflage their activities amongst those."); - conversation.addPiece(contact_character, "I don't know what happened of the ship relics themselves, they disappeared, but all you see left are their quantum matrix computers."); - conversation.addPiece(null, "This makes no sense, ship bodies have no value without their brains!"); - conversation.addPiece(contact_character, "Exactly. Someone went through a lot of work, but I do not see the reason why."); - conversation.addPiece(null, "Do you have any clue on where the ships were taken?"); - conversation.addPiece(contact_character, "From what I gathered from locals, they were taken far away, maybe to the other side of the galaxy."); - conversation.addPiece(null, "So many ship structures stripped and transported elsewhere is weird. You're right, it's a LOT of work. We definitely need to know what is going on with it."); - conversation.addPiece(null, "..."); - conversation.addPiece(null, "But it seems that we will have to deal with something else first! My high-spectrum sensors just picked an incoming fleet that will emerge near us."); - - // Fight with the patrol - this.addPart(new MissionPartCleanLocation(this, graveyard_location, "Fight the arrived fleet")); - } - } +function randomLocation(random: RandomGenerator, stars: Star[], excludes: StarLocation[] = []) { + let star = stars.length == 1 ? stars[0] : random.choice(stars); + return RandomGenerator.global.choice(star.locations.filter(loc => !contains(excludes, loc))); +} + +/** + * Main story arc + */ +export class MainStory extends Mission { + constructor(universe: Universe, fleet: Fleet) { + super(universe, fleet, true); + + let random = RandomGenerator.global; + let start_location = nn(universe.getLocation(fleet.location)); + let mission_generator = new MissionGenerator(universe, start_location); + + // Arrival + let conversation = this.addPart(new MissionPartConversation(this, [], "Travel to Terranax galaxy")); + conversation.addPiece(null, "Wow ! From what my sensors tell me, there is not much activity around here."); + conversation.addPiece(null, "I remember the last time I came in this galaxy, you needed to be aware of collisions at all time, so crowded it was."); + conversation.addPiece(null, "Well...I did not pick a signal from our contact yet. We should be looking for her in this system."); + + // Get in touch with our contact + let contact_location = randomLocation(random, [start_location.star], [start_location]); + let contact_character = mission_generator.generateShip(1); + contact_character.fleet.setLocation(contact_location); + this.addPart(new MissionPartGoTo(this, contact_location, `Find your contact in ${contact_location.star.name}`, MissionPartDestinationHint.SYSTEM)); + conversation = this.addPart(new MissionPartConversation(this, [contact_character], "Speak with your contact")); + conversation.addPiece(contact_character, "Finally, you came!"); + conversation.addPiece(contact_character, "Sorry for not broadcasting my position. As you may have encountered, this star system is not safe anymore."); + conversation.addPiece(null, "Nothing we could not handle, we just hope the other teams have not run across more trouble."); + conversation.addPiece(contact_character, "I do not even know if the other contacts made it to their rendezvous point. Jumping between systems has become quite a hassle around here."); + conversation.addPiece(null, "And we still do not know why those rogue fleets are trying to lockdown the whole galaxy? Did you have some interaction with them?"); + conversation.addPiece(contact_character, "Well, they tend to shoot you on sight if you go near a location they defend. Do not know if that qualifies as interaction though..."); + conversation.addPiece(null, "So where do we go from here? In your last message, you told us of a resistance group growing."); + conversation.addPiece(contact_character, "Yes, some merchants and miners have rallied behind a retired TSF general, but I lost contact with them weeks ago."); + conversation.addPiece(contact_character, "We may go to their last known location, but first I want you to see something in a nearby system."); + conversation.addPiece(contact_character, "If you need any equipment before going, there is a dockyard in this system. They often offer some missions if you need money too."); + conversation.addPiece(null, "Yes, we came here with basic equipment as to not raise suspicion, but it looks like we will need more to defend ourselves."); + conversation.addPiece(null, "Ok, let's go..."); + + // Go take a look at the graveyard + let nearby_systems = nna(start_location.star.getLinks().map(link => link.getPeer(contact_location.star))); + let graveyard_location = randomLocation(random, [minBy(nearby_systems, system => system.level)]); + this.addPart(new MissionPartEscort(this, graveyard_location, contact_character, `Go with ${contact_character.name} in ${graveyard_location.star.name} system`)); + conversation = this.addPart(new MissionPartConversation(this, [contact_character], `Listen to ${contact_character.name}`)); + conversation.addPiece(null, "What is all this junk?"); + conversation.addPiece(contact_character, "This was until recently an unofficial ship scrap yard. Lots of abandoned ship carcasses were floating in here, and many smugglers used to camouflage their activities amongst those."); + conversation.addPiece(contact_character, "I don't know what happened of the ship relics themselves, they disappeared, but all you see left are their quantum matrix computers."); + conversation.addPiece(null, "This makes no sense, ship bodies have no value without their brains!"); + conversation.addPiece(contact_character, "Exactly. Someone went through a lot of work, but I do not see the reason why."); + conversation.addPiece(null, "Do you have any clue on where the ships were taken?"); + conversation.addPiece(contact_character, "From what I gathered from locals, they were taken far away, maybe to the other side of the galaxy."); + conversation.addPiece(null, "So many ship structures stripped and transported elsewhere is weird. You're right, it's a LOT of work. We definitely need to know what is going on with it."); + conversation.addPiece(null, "..."); + conversation.addPiece(null, "But it seems that we will have to deal with something else first! My high-spectrum sensors just picked an incoming fleet that will emerge near us."); + + // Fight with the patrol + this.addPart(new MissionPartCleanLocation(this, graveyard_location, "Fight the arrived fleet")); + } } diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts index 627c1e4..5b79a31 100644 --- a/src/core/missions/Mission.spec.ts +++ b/src/core/missions/Mission.spec.ts @@ -1,58 +1,63 @@ -module TK.SpaceTac.Specs { - testing("Mission", test => { - test.case("check step status", check => { - let universe = new Universe(); - let fleet = new Fleet(); - let mission = new Mission(universe, fleet); - mission.addPart(new MissionPart(mission, "Part 1")); - mission.addPart(new MissionPart(mission, "Part 2")); +import { testing } from "../../common/Testing"; +import { iterator, nnf } from "../../common/Tools"; +import { Fleet } from "../Fleet"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionPart } from "./MissionPart"; - check.same(mission.current_part, mission.parts[0]); +testing("Mission", test => { + test.case("check step status", check => { + let universe = new Universe(); + let fleet = new Fleet(); + let mission = new Mission(universe, fleet); + mission.addPart(new MissionPart(mission, "Part 1")); + mission.addPart(new MissionPart(mission, "Part 2")); - let result = mission.checkStatus(); - check.equals(result, true); - check.same(mission.current_part, mission.parts[0]); + check.same(mission.current_part, mission.parts[0]); - check.patch(mission.parts[0], "checkCompleted", nnf(true, iterator([false, true]))); + let result = mission.checkStatus(); + check.equals(result, true); + check.same(mission.current_part, mission.parts[0]); - result = mission.checkStatus(); - check.equals(result, true); - check.same(mission.current_part, mission.parts[0]); - result = mission.checkStatus(); - check.equals(result, true); - check.same(mission.current_part, mission.parts[1]); - result = mission.checkStatus(); - check.equals(result, true); - check.same(mission.current_part, mission.parts[1]); + check.patch(mission.parts[0], "checkCompleted", nnf(true, iterator([false, true]))); - check.patch(mission.parts[1], "checkCompleted", () => true); + result = mission.checkStatus(); + check.equals(result, true); + check.same(mission.current_part, mission.parts[0]); + result = mission.checkStatus(); + check.equals(result, true); + check.same(mission.current_part, mission.parts[1]); + result = mission.checkStatus(); + check.equals(result, true); + check.same(mission.current_part, mission.parts[1]); - result = mission.checkStatus(); - check.equals(result, false); - check.same(mission.current_part, mission.parts[1]); - }) + check.patch(mission.parts[1], "checkCompleted", () => true); - test.case("stores a reward", check => { - let mission = new Mission(new Universe()); - check.equals(mission.getRewardText(), "-"); + result = mission.checkStatus(); + check.equals(result, false); + check.same(mission.current_part, mission.parts[1]); + }) - mission.reward = 720; - check.equals(mission.getRewardText(), "720 zotys"); - }) + test.case("stores a reward", check => { + let mission = new Mission(new Universe()); + check.equals(mission.getRewardText(), "-"); - test.case("gives the reward on completion", check => { - let fleet = new Fleet(); - let ship = fleet.addShip(); - fleet.credits = 150; + mission.reward = 720; + check.equals(mission.getRewardText(), "720 zotys"); + }) - let mission = new Mission(new Universe(), fleet); - mission.reward = 75; - mission.setCompleted(); - check.equals(mission.completed, true); - check.equals(fleet.credits, 225); + test.case("gives the reward on completion", check => { + let fleet = new Fleet(); + let ship = fleet.addShip(); + fleet.credits = 150; - mission.setCompleted(); - check.equals(fleet.credits, 225); - }) - }) -} + let mission = new Mission(new Universe(), fleet); + mission.reward = 75; + mission.setCompleted(); + check.equals(mission.completed, true); + check.equals(fleet.credits, 225); + + mission.setCompleted(); + check.equals(fleet.credits, 225); + }) +}) diff --git a/src/core/missions/Mission.ts b/src/core/missions/Mission.ts index 9c75b55..2923dee 100644 --- a/src/core/missions/Mission.ts +++ b/src/core/missions/Mission.ts @@ -1,150 +1,152 @@ -module TK.SpaceTac { - /** - * Reward for a mission (currently, only money) - */ - export type MissionReward = number +import { Fleet } from "../Fleet" +import { Universe } from "../Universe" +import { MissionPart } from "./MissionPart" - /** - * Level of difficulty for a mission - */ - export enum MissionDifficulty { - easy, - normal, - hard - } +/** + * Reward for a mission (currently, only money) + */ +export type MissionReward = number - /** - * A mission (or quest) assigned to the player - */ - export class Mission { - // Link to the fleet this mission has been assigned to - fleet: Fleet - - // Link to the universe in which the mission plays - universe: Universe - - // Indicator that the quest is part of the main story arc - main: boolean - - // Parts of the mission - parts: MissionPart[] - - // Current part - current_part: MissionPart - - // Indicator that the mission is completed - completed: boolean - - // Title of this mission (should be kept short) - title: string - - // Estimated mission difficulty and value (expected reward value) - difficulty: MissionDifficulty = MissionDifficulty.normal - value = 0 - - // Reward when this mission is completed - reward: MissionReward | null = null - - // Numerical identifier - id = -1 - - constructor(universe: Universe, fleet = new Fleet(), main = false) { - this.universe = universe; - this.fleet = fleet; - this.main = main; - this.parts = []; - this.completed = false; - this.current_part = new MissionPart(this, "Empty mission"); - this.title = main ? "Main story" : "Secondary mission"; - } - - /** - * Add a part to the mission. - */ - addPart(part: T): T { - if (part.mission === this) { - this.parts.push(part); - if (this.parts.length == 1) { - this.current_part = this.parts[0]; - } - this.completed = false; - } - return part; - } - - /** - * Get the index of current part - */ - getIndex(): number { - return this.parts.indexOf(this.current_part); - } - - /** - * Get a small text describing the associated reward - */ - getRewardText(): string { - if (this.reward) { - return `${this.reward} zotys`; - } else { - return "-"; - } - } - - /** - * Set the difficulty level - */ - setDifficulty(description: MissionDifficulty, value: number) { - this.difficulty = description; - this.value = value; - } - - /** - * Set the mission as started (start the first part) - */ - setStarted(id: number): void { - if (this.id < 0) { - this.id = id; - if (this.current_part) { - this.current_part.onStarted(); - } - } - } - - /** - * Check the status for current part, and move on to next part if necessary. - * - * Returns true if the mission is still active. - */ - checkStatus(): boolean { - if (this.completed) { - return false; - } else if (this.current_part.checkCompleted()) { - this.current_part.onEnded(); - - let current_index = this.getIndex(); - if (current_index < 0 || current_index >= this.parts.length - 1) { - this.setCompleted(); - return false; - } else { - this.current_part = this.parts[current_index + 1]; - this.current_part.onStarted(); - return true; - } - } else { - return true; - } - } - - /** - * Set the mission as completed, and give the reward to the fleet - */ - setCompleted(): void { - if (!this.completed) { - this.completed = true; - if (this.reward) { - this.fleet.credits += this.reward; - } - } - } - } +/** + * Level of difficulty for a mission + */ +export enum MissionDifficulty { + easy, + normal, + hard +} + +/** + * A mission (or quest) assigned to the player + */ +export class Mission { + // Link to the fleet this mission has been assigned to + fleet: Fleet + + // Link to the universe in which the mission plays + universe: Universe + + // Indicator that the quest is part of the main story arc + main: boolean + + // Parts of the mission + parts: MissionPart[] + + // Current part + current_part: MissionPart + + // Indicator that the mission is completed + completed: boolean + + // Title of this mission (should be kept short) + title: string + + // Estimated mission difficulty and value (expected reward value) + difficulty: MissionDifficulty = MissionDifficulty.normal + value = 0 + + // Reward when this mission is completed + reward: MissionReward | null = null + + // Numerical identifier + id = -1 + + constructor(universe: Universe, fleet = new Fleet(), main = false) { + this.universe = universe; + this.fleet = fleet; + this.main = main; + this.parts = []; + this.completed = false; + this.current_part = new MissionPart(this, "Empty mission"); + this.title = main ? "Main story" : "Secondary mission"; + } + + /** + * Add a part to the mission. + */ + addPart(part: T): T { + if (part.mission === this) { + this.parts.push(part); + if (this.parts.length == 1) { + this.current_part = this.parts[0]; + } + this.completed = false; + } + return part; + } + + /** + * Get the index of current part + */ + getIndex(): number { + return this.parts.indexOf(this.current_part); + } + + /** + * Get a small text describing the associated reward + */ + getRewardText(): string { + if (this.reward) { + return `${this.reward} zotys`; + } else { + return "-"; + } + } + + /** + * Set the difficulty level + */ + setDifficulty(description: MissionDifficulty, value: number) { + this.difficulty = description; + this.value = value; + } + + /** + * Set the mission as started (start the first part) + */ + setStarted(id: number): void { + if (this.id < 0) { + this.id = id; + if (this.current_part) { + this.current_part.onStarted(); + } + } + } + + /** + * Check the status for current part, and move on to next part if necessary. + * + * Returns true if the mission is still active. + */ + checkStatus(): boolean { + if (this.completed) { + return false; + } else if (this.current_part.checkCompleted()) { + this.current_part.onEnded(); + + let current_index = this.getIndex(); + if (current_index < 0 || current_index >= this.parts.length - 1) { + this.setCompleted(); + return false; + } else { + this.current_part = this.parts[current_index + 1]; + this.current_part.onStarted(); + return true; + } + } else { + return true; + } + } + + /** + * Set the mission as completed, and give the reward to the fleet + */ + setCompleted(): void { + if (!this.completed) { + this.completed = true; + if (this.reward) { + this.fleet.credits += this.reward; + } + } + } } diff --git a/src/core/missions/MissionGenerator.spec.ts b/src/core/missions/MissionGenerator.spec.ts index 2969286..3ce77c9 100644 --- a/src/core/missions/MissionGenerator.spec.ts +++ b/src/core/missions/MissionGenerator.spec.ts @@ -1,80 +1,88 @@ -module TK.SpaceTac.Specs { - testing("MissionGenerator", test => { - test.case("generates escort missions", check => { - let universe = new Universe(); - let star1 = universe.addStar(1); - let loc1 = star1.locations[0]; - let star2 = universe.addStar(2); - let loc2 = star2.locations[0]; - universe.addLink(star1, star2); +import { testing } from "../../common/Testing"; +import { StarLocation, StarLocationType } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission, MissionDifficulty } from "./Mission"; +import { MissionGenerator } from "./MissionGenerator"; +import { MissionPartCleanLocation } from "./MissionPartCleanLocation"; +import { MissionPartConversation } from "./MissionPartConversation"; +import { MissionPartEscort } from "./MissionPartEscort"; +import { MissionPartGoTo } from "./MissionPartGoTo"; - let generator = new MissionGenerator(universe, loc1); - let mission = generator.generateEscort(); +testing("MissionGenerator", test => { + test.case("generates escort missions", check => { + let universe = new Universe(); + let star1 = universe.addStar(1); + let loc1 = star1.locations[0]; + let star2 = universe.addStar(2); + let loc2 = star2.locations[0]; + universe.addLink(star1, star2); - check.equals(mission.title, "Escort a ship to a level 2 system"); - check.equals(mission.parts.length, 3); - check.equals(mission.parts[0] instanceof MissionPartConversation, true); - check.equals(mission.parts[1] instanceof MissionPartEscort, true); - let escort = mission.parts[1]; - check.same(escort.destination, loc2); - check.equals(escort.ship.level.get(), 2); - check.equals(mission.parts[2] instanceof MissionPartConversation, true); - }) + let generator = new MissionGenerator(universe, loc1); + let mission = generator.generateEscort(); - test.case("generates location cleaning missions", check => { - let universe = new Universe(); - let star1 = universe.addStar(1, "TTX"); - let loc1 = star1.locations[0]; - let loc2 = star1.addLocation(StarLocationType.PLANET); + check.equals(mission.title, "Escort a ship to a level 2 system"); + check.equals(mission.parts.length, 3); + check.equals(mission.parts[0] instanceof MissionPartConversation, true); + check.equals(mission.parts[1] instanceof MissionPartEscort, true); + let escort = mission.parts[1]; + check.same(escort.destination, loc2); + check.equals(escort.ship.level.get(), 2); + check.equals(mission.parts[2] instanceof MissionPartConversation, true); + }) - let generator = new MissionGenerator(universe, loc1); - let mission = generator.generateCleanLocation(); + test.case("generates location cleaning missions", check => { + let universe = new Universe(); + let star1 = universe.addStar(1, "TTX"); + let loc1 = star1.locations[0]; + let loc2 = star1.addLocation(StarLocationType.PLANET); - check.equals(mission.title, "Defeat a level 1 fleet in this system"); - check.equals(mission.parts.length, 4); - check.equals(mission.parts[0] instanceof MissionPartConversation, true); - check.equals(mission.parts[1] instanceof MissionPartCleanLocation, true); - let part1 = mission.parts[1]; - check.same(part1.destination, loc2); - check.equals(part1.title, "Clean a planet in TTX system"); - check.equals(mission.parts[2] instanceof MissionPartGoTo, true); - let part2 = mission.parts[2]; - check.same(part2.destination, loc1); - check.equals(part2.title, "Go back to collect your reward"); - check.equals(mission.parts[3] instanceof MissionPartConversation, true); - }) + let generator = new MissionGenerator(universe, loc1); + let mission = generator.generateCleanLocation(); - test.case("helps to evaluate mission difficulty", check => { - let generator = new MissionGenerator(new Universe(), new StarLocation()); - let mission = new Mission(generator.universe); - check.same(mission.difficulty, MissionDifficulty.normal); - check.equals(mission.value, 0); + check.equals(mission.title, "Defeat a level 1 fleet in this system"); + check.equals(mission.parts.length, 4); + check.equals(mission.parts[0] instanceof MissionPartConversation, true); + check.equals(mission.parts[1] instanceof MissionPartCleanLocation, true); + let part1 = mission.parts[1]; + check.same(part1.destination, loc2); + check.equals(part1.title, "Clean a planet in TTX system"); + check.equals(mission.parts[2] instanceof MissionPartGoTo, true); + let part2 = mission.parts[2]; + check.same(part2.destination, loc1); + check.equals(part2.title, "Go back to collect your reward"); + check.equals(mission.parts[3] instanceof MissionPartConversation, true); + }) - generator.setDifficulty(mission, 1000, 1); - check.same(mission.difficulty, MissionDifficulty.normal); - check.equals(mission.value, 1000); + test.case("helps to evaluate mission difficulty", check => { + let generator = new MissionGenerator(new Universe(), new StarLocation()); + let mission = new Mission(generator.universe); + check.same(mission.difficulty, MissionDifficulty.normal); + check.equals(mission.value, 0); - generator.setDifficulty(mission, 1000, 2); - check.same(mission.difficulty, MissionDifficulty.hard); - check.equals(mission.value, 2200); + generator.setDifficulty(mission, 1000, 1); + check.same(mission.difficulty, MissionDifficulty.normal); + check.equals(mission.value, 1000); - generator.setDifficulty(mission, 1000, 3); - check.same(mission.difficulty, MissionDifficulty.hard); - check.equals(mission.value, 3600); + generator.setDifficulty(mission, 1000, 2); + check.same(mission.difficulty, MissionDifficulty.hard); + check.equals(mission.value, 2200); - generator.around.star.level = 10; + generator.setDifficulty(mission, 1000, 3); + check.same(mission.difficulty, MissionDifficulty.hard); + check.equals(mission.value, 3600); - generator.setDifficulty(mission, 1000, 10); - check.same(mission.difficulty, MissionDifficulty.normal); - check.equals(mission.value, 10000); + generator.around.star.level = 10; - generator.setDifficulty(mission, 1000, 9); - check.same(mission.difficulty, MissionDifficulty.easy); - check.equals(mission.value, 8100); + generator.setDifficulty(mission, 1000, 10); + check.same(mission.difficulty, MissionDifficulty.normal); + check.equals(mission.value, 10000); - generator.setDifficulty(mission, 1000, 8); - check.same(mission.difficulty, MissionDifficulty.easy); - check.equals(mission.value, 6400); - }) - }); -} + generator.setDifficulty(mission, 1000, 9); + check.same(mission.difficulty, MissionDifficulty.easy); + check.equals(mission.value, 8100); + + generator.setDifficulty(mission, 1000, 8); + check.same(mission.difficulty, MissionDifficulty.easy); + check.equals(mission.value, 6400); + }) +}); diff --git a/src/core/missions/MissionGenerator.ts b/src/core/missions/MissionGenerator.ts index e004481..f91455a 100644 --- a/src/core/missions/MissionGenerator.ts +++ b/src/core/missions/MissionGenerator.ts @@ -1,158 +1,167 @@ -module TK.SpaceTac { - const POOL_SHIP_NAMES = [ - "Zert", "Zark", "Zeem", - "Ob'tec", "Ob'vac", "Ob'sig", - "Paayk", "Paakt", - "Fen_amr", "Fin_am", "Fen_AA", - "TempZst", "TriZth", - "croNt", "coRzt", - "Appn", "Appq", - "Vertix", "Vortix", - "Opan-vel", "Ipal-ven", "Epan-vek", - "Yz-aol", "Yz-aib", - "Arkant", "Arkyan", - "PNX", "PGV", "PXT", "PRZ", - ] +import { RandomGenerator } from "../../common/RandomGenerator"; +import { bound, capitalize, clamp } from "../../common/Tools"; +import { ShipGenerator } from "../ShipGenerator"; +import { StarLocation, StarLocationType } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission, MissionDifficulty, MissionReward } from "./Mission"; +import { MissionPartCleanLocation } from "./MissionPartCleanLocation"; +import { MissionPartConversation } from "./MissionPartConversation"; +import { MissionPartEscort } from "./MissionPartEscort"; +import { MissionPartGoTo } from "./MissionPartGoTo"; - /** - * Random generator of secondary missions that can be taken from - */ - export class MissionGenerator { - universe: Universe - around: StarLocation - random: RandomGenerator +const POOL_SHIP_NAMES = [ + "Zert", "Zark", "Zeem", + "Ob'tec", "Ob'vac", "Ob'sig", + "Paayk", "Paakt", + "Fen_amr", "Fin_am", "Fen_AA", + "TempZst", "TriZth", + "croNt", "coRzt", + "Appn", "Appq", + "Vertix", "Vortix", + "Opan-vel", "Ipal-ven", "Epan-vek", + "Yz-aol", "Yz-aib", + "Arkant", "Arkyan", + "PNX", "PGV", "PXT", "PRZ", +] - constructor(universe: Universe, around: StarLocation, random = RandomGenerator.global) { - this.universe = universe; - this.around = around; - this.random = random; - } +/** + * Random generator of secondary missions that can be taken from + */ +export class MissionGenerator { + universe: Universe + around: StarLocation + random: RandomGenerator - /** - * Generate a single mission - */ - generate(): Mission { - let generators = [ - bound(this, "generateEscort"), - bound(this, "generateCleanLocation"), - ]; + constructor(universe: Universe, around: StarLocation, random = RandomGenerator.global) { + this.universe = universe; + this.around = around; + this.random = random; + } - let generator = this.random.choice(generators); - let result = generator(); - if (result.value) { - result.reward = this.generateReward(result.value); - } - return result; - } + /** + * Generate a single mission + */ + generate(): Mission { + let generators = [ + bound(this, "generateEscort"), + bound(this, "generateCleanLocation"), + ]; - /** - * Generate a character name - */ - static generateCharacterName(random = RandomGenerator.global): string { - return `${random.choice(POOL_SHIP_NAMES)}-${random.randInt(10, 999)}`; - } - - /** - * Generate a new ship that may be used in a mission - */ - generateShip(level: number) { - let generator = new ShipGenerator(this.random); - let result = generator.generate(level, null, true); - result.name = MissionGenerator.generateCharacterName(this.random); - return result; - } - - /** - * Generate a reward - */ - generateReward(value: number): MissionReward { - // TODO - return value; - } - - /** - * Helper to set the difficulty of a mission - */ - setDifficulty(mission: Mission, base_value: number, fight_level: number) { - let level_diff = fight_level - this.around.star.level; - let code = (level_diff > 0) ? MissionDifficulty.hard : (level_diff < 0 ? MissionDifficulty.easy : MissionDifficulty.normal); - let value = fight_level * (base_value + base_value * 0.1 * clamp(level_diff, -5, 5)); - mission.setDifficulty(code, Math.round(value)); - } - - /** - * Generate an escort mission - */ - generateEscort(): Mission { - let mission = new Mission(this.universe); - - let dest_star = this.random.choice(this.around.star.getNeighbors()); - let destination = this.random.choice(dest_star.locations); - let ship = this.generateShip(dest_star.level); - - mission.title = `Escort a ship to a level ${dest_star.level} system`; - this.setDifficulty(mission, 1000, dest_star.level); - - let conversation = new MissionPartConversation(mission, [ship]); - conversation.addPiece(ship, this.random.choice([ - "I'm very grateful you accepted to escort me! These systems are not safe to travel anymore.", - "Thank you for bringing me along, I'm afraid not to be up to the dangers ahead.", - "Always a pleasure to travel with battle-ready companions, it is not a triviality these days!", - ])); - mission.addPart(conversation); - - mission.addPart(new MissionPartEscort(mission, destination, ship)); - - conversation = new MissionPartConversation(mission, [ship]); - conversation.addPiece(ship, this.random.choice([ - "Thank you! Have your reward, you deserved it.", - "Never could have made it safely without your help, take this as a token of my gratitude.", - "Thanks so much... May we meet again in the future. For now, please accept this small reward.", - ])); - mission.addPart(conversation); - - return mission; - } - - /** - * Generate a clean location mission - */ - generateCleanLocation(): Mission { - let mission = new Mission(this.universe); - - let dest_star = this.random.choice(this.around.star.getNeighbors().concat([this.around.star])); - let here = (dest_star == this.around.star); - let choices = dest_star.locations; - if (here) { - choices = choices.filter(loc => loc != this.around); - } - let destination = this.random.choice(choices); - let ship = this.generateShip(1); - - mission.title = `Defeat a level ${destination.star.level} fleet in ${here ? "this" : "a nearby"} system`; - this.setDifficulty(mission, here ? 300 : 500, dest_star.level); - - let conversation = new MissionPartConversation(mission, [ship]); - conversation.addPiece(ship, this.random.choice([ - `We need you to clean a ${capitalize(StarLocationType[destination.type].toLowerCase())} for us. It is of vital importance.`, - "It would be very kind of you to remove these pesky rogues from the location we pointed on your map. Have no mercy!", - "One of those uninvited fleets is blocking a strategic point for our supplies. Send them back to hell.", - ])); - mission.addPart(conversation); - - mission.addPart(new MissionPartCleanLocation(mission, destination)); - - mission.addPart(new MissionPartGoTo(mission, this.around, "Go back to collect your reward")); - - conversation = new MissionPartConversation(mission, [ship]); - conversation.addPiece(ship, this.random.choice([ - "You really are efficient! Feel free to have a look at our other jobs, while we load your reward.", - "We know it will be a temporary respite, but thank you nonetheless.", - "A job well done, is a job well paid! Looking forward to working with you again.", - ])); - mission.addPart(conversation); - - return mission; - } + let generator = this.random.choice(generators); + let result = generator(); + if (result.value) { + result.reward = this.generateReward(result.value); } + return result; + } + + /** + * Generate a character name + */ + static generateCharacterName(random = RandomGenerator.global): string { + return `${random.choice(POOL_SHIP_NAMES)}-${random.randInt(10, 999)}`; + } + + /** + * Generate a new ship that may be used in a mission + */ + generateShip(level: number) { + let generator = new ShipGenerator(this.random); + let result = generator.generate(level, null, true); + result.name = MissionGenerator.generateCharacterName(this.random); + return result; + } + + /** + * Generate a reward + */ + generateReward(value: number): MissionReward { + // TODO + return value; + } + + /** + * Helper to set the difficulty of a mission + */ + setDifficulty(mission: Mission, base_value: number, fight_level: number) { + let level_diff = fight_level - this.around.star.level; + let code = (level_diff > 0) ? MissionDifficulty.hard : (level_diff < 0 ? MissionDifficulty.easy : MissionDifficulty.normal); + let value = fight_level * (base_value + base_value * 0.1 * clamp(level_diff, -5, 5)); + mission.setDifficulty(code, Math.round(value)); + } + + /** + * Generate an escort mission + */ + generateEscort(): Mission { + let mission = new Mission(this.universe); + + let dest_star = this.random.choice(this.around.star.getNeighbors()); + let destination = this.random.choice(dest_star.locations); + let ship = this.generateShip(dest_star.level); + + mission.title = `Escort a ship to a level ${dest_star.level} system`; + this.setDifficulty(mission, 1000, dest_star.level); + + let conversation = new MissionPartConversation(mission, [ship]); + conversation.addPiece(ship, this.random.choice([ + "I'm very grateful you accepted to escort me! These systems are not safe to travel anymore.", + "Thank you for bringing me along, I'm afraid not to be up to the dangers ahead.", + "Always a pleasure to travel with battle-ready companions, it is not a triviality these days!", + ])); + mission.addPart(conversation); + + mission.addPart(new MissionPartEscort(mission, destination, ship)); + + conversation = new MissionPartConversation(mission, [ship]); + conversation.addPiece(ship, this.random.choice([ + "Thank you! Have your reward, you deserved it.", + "Never could have made it safely without your help, take this as a token of my gratitude.", + "Thanks so much... May we meet again in the future. For now, please accept this small reward.", + ])); + mission.addPart(conversation); + + return mission; + } + + /** + * Generate a clean location mission + */ + generateCleanLocation(): Mission { + let mission = new Mission(this.universe); + + let dest_star = this.random.choice(this.around.star.getNeighbors().concat([this.around.star])); + let here = (dest_star == this.around.star); + let choices = dest_star.locations; + if (here) { + choices = choices.filter(loc => loc != this.around); + } + let destination = this.random.choice(choices); + let ship = this.generateShip(1); + + mission.title = `Defeat a level ${destination.star.level} fleet in ${here ? "this" : "a nearby"} system`; + this.setDifficulty(mission, here ? 300 : 500, dest_star.level); + + let conversation = new MissionPartConversation(mission, [ship]); + conversation.addPiece(ship, this.random.choice([ + `We need you to clean a ${capitalize(StarLocationType[destination.type].toLowerCase())} for us. It is of vital importance.`, + "It would be very kind of you to remove these pesky rogues from the location we pointed on your map. Have no mercy!", + "One of those uninvited fleets is blocking a strategic point for our supplies. Send them back to hell.", + ])); + mission.addPart(conversation); + + mission.addPart(new MissionPartCleanLocation(mission, destination)); + + mission.addPart(new MissionPartGoTo(mission, this.around, "Go back to collect your reward")); + + conversation = new MissionPartConversation(mission, [ship]); + conversation.addPiece(ship, this.random.choice([ + "You really are efficient! Feel free to have a look at our other jobs, while we load your reward.", + "We know it will be a temporary respite, but thank you nonetheless.", + "A job well done, is a job well paid! Looking forward to working with you again.", + ])); + mission.addPart(conversation); + + return mission; + } } diff --git a/src/core/missions/MissionPart.ts b/src/core/missions/MissionPart.ts index 25f8340..1096e44 100644 --- a/src/core/missions/MissionPart.ts +++ b/src/core/missions/MissionPart.ts @@ -1,58 +1,62 @@ -module TK.SpaceTac { - /** - * An abstract part of a mission, describing the goal - */ - export class MissionPart { - // Link to mission - mission: Mission +import { Fleet } from "../Fleet"; +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; - // Very short description - title: string +/** + * An abstract part of a mission, describing the goal + */ +export class MissionPart { + // Link to mission + mission: Mission - constructor(mission: Mission, title: string) { - this.mission = mission; - this.title = title; - } + // Very short description + title: string - get universe(): Universe { - return this.mission.universe; - } - get fleet(): Fleet { - return this.mission.fleet; - } + constructor(mission: Mission, title: string) { + this.mission = mission; + this.title = title; + } - /** - * Abstract checking if the part is completed - */ - checkCompleted(): boolean { - return false; - } + get universe(): Universe { + return this.mission.universe; + } + get fleet(): Fleet { + return this.mission.fleet; + } - /** - * Force the part as completed - * - * This is a cheat, and should enforce the part conditions - */ - forceComplete(): void { - } + /** + * Abstract checking if the part is completed + */ + checkCompleted(): boolean { + return false; + } - /** - * Get a location hint about this part - */ - getLocationHint(): Star | StarLocation | null { - return null; - } + /** + * Force the part as completed + * + * This is a cheat, and should enforce the part conditions + */ + forceComplete(): void { + } - /** - * Event called when the part starts - */ - onStarted(): void { - } + /** + * Get a location hint about this part + */ + getLocationHint(): Star | StarLocation | null { + return null; + } - /** - * Event called when the part ends - */ - onEnded(): void { - } - } + /** + * Event called when the part starts + */ + onStarted(): void { + } + + /** + * Event called when the part ends + */ + onEnded(): void { + } } diff --git a/src/core/missions/MissionPartCleanLocation.spec.ts b/src/core/missions/MissionPartCleanLocation.spec.ts index b70c8a6..36c4510 100644 --- a/src/core/missions/MissionPartCleanLocation.spec.ts +++ b/src/core/missions/MissionPartCleanLocation.spec.ts @@ -1,41 +1,48 @@ -module TK.SpaceTac.Specs { - testing("MissionPartEscort", test => { - test.case("completes when the fleet is at location, and the encounter is clean", check => { - let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); - destination.clearEncounter(); +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { Fleet } from "../Fleet"; +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionPartCleanLocation } from "./MissionPartCleanLocation"; - let universe = new Universe(); - let fleet = new Fleet(); - let part = new MissionPartCleanLocation(new Mission(universe, fleet), destination); +testing("MissionPartEscort", test => { + test.case("completes when the fleet is at location, and the encounter is clean", check => { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.clearEncounter(); - check.equals(part.title, "Clean a planet in Atanax system"); - check.same(part.checkCompleted(), false, "Init location"); + let universe = new Universe(); + let fleet = new Fleet(); + let part = new MissionPartCleanLocation(new Mission(universe, fleet), destination); - check.equals(destination.isClear(), true); - part.onStarted(); - check.equals(destination.isClear(), false); + check.equals(part.title, "Clean a planet in Atanax system"); + check.same(part.checkCompleted(), false, "Init location"); - fleet.setLocation(destination); - check.same(part.checkCompleted(), false, "Encounter not clear"); + check.equals(destination.isClear(), true); + part.onStarted(); + check.equals(destination.isClear(), false); - destination.clearEncounter(); - check.same(part.checkCompleted(), true, "Encouter cleared"); - }) + fleet.setLocation(destination); + check.same(part.checkCompleted(), false, "Encounter not clear"); - test.case("generates the battle immediately if the fleet is already at the destination", check => { - let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); - destination.clearEncounter(); + destination.clearEncounter(); + check.same(part.checkCompleted(), true, "Encouter cleared"); + }) - let universe = new Universe(); - let fleet = new Fleet(); - fleet.setLocation(destination); - let part = new MissionPartCleanLocation(new Mission(universe, fleet), destination); + test.case("generates the battle immediately if the fleet is already at the destination", check => { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.clearEncounter(); - check.equals(fleet.battle, null); - part.onStarted(); - check.notequals(fleet.battle, null); - check.equals(nn(fleet.battle).fleets, [fleet, nn(destination.encounter)]); - check.equals(part.checkCompleted(), false); - }) - }) -} + let universe = new Universe(); + let fleet = new Fleet(); + fleet.setLocation(destination); + let part = new MissionPartCleanLocation(new Mission(universe, fleet), destination); + + check.equals(fleet.battle, null); + part.onStarted(); + check.notequals(fleet.battle, null); + check.equals(nn(fleet.battle).fleets, [fleet, nn(destination.encounter)]); + check.equals(part.checkCompleted(), false); + }) +}) diff --git a/src/core/missions/MissionPartCleanLocation.ts b/src/core/missions/MissionPartCleanLocation.ts index 5653257..914ba8d 100644 --- a/src/core/missions/MissionPartCleanLocation.ts +++ b/src/core/missions/MissionPartCleanLocation.ts @@ -1,28 +1,28 @@ -/// +import { StarLocation, StarLocationType } from "../StarLocation"; +import { Mission } from "./Mission"; +import { MissionPartGoTo } from "./MissionPartGoTo"; -module TK.SpaceTac { - /** - * A mission part that requires the fleet to clean a specific location of enemies - */ - export class MissionPartCleanLocation extends MissionPartGoTo { - constructor(mission: Mission, destination: StarLocation, directive?: string) { - super(mission, destination, directive || `Clean a ${StarLocationType[destination.type].toLowerCase()} in ${destination.star.name} system`); - } +/** + * A mission part that requires the fleet to clean a specific location of enemies + */ +export class MissionPartCleanLocation extends MissionPartGoTo { + constructor(mission: Mission, destination: StarLocation, directive?: string) { + super(mission, destination, directive || `Clean a ${StarLocationType[destination.type].toLowerCase()} in ${destination.star.name} system`); + } - checkCompleted(): boolean { - return super.checkCompleted() && this.destination.isClear(); - } + checkCompleted(): boolean { + return super.checkCompleted() && this.destination.isClear(); + } - onStarted(): void { - this.destination.setupEncounter(); + onStarted(): void { + this.destination.setupEncounter(); - if (this.destination.is(this.fleet.location)) { - // Already there, re-enter the location to start the fight - let battle = this.destination.enterLocation(this.fleet); - if (battle) { - this.fleet.setBattle(battle); - } - } - } + if (this.destination.is(this.fleet.location)) { + // Already there, re-enter the location to start the fight + let battle = this.destination.enterLocation(this.fleet); + if (battle) { + this.fleet.setBattle(battle); + } } + } } diff --git a/src/core/missions/MissionPartConversation.spec.ts b/src/core/missions/MissionPartConversation.spec.ts index ed805f7..5c42c19 100644 --- a/src/core/missions/MissionPartConversation.spec.ts +++ b/src/core/missions/MissionPartConversation.spec.ts @@ -1,52 +1,57 @@ -module TK.SpaceTac.Specs { - testing("MissionPartConversation", test => { - test.case("advances through conversation", check => { - let universe = new Universe(); - let fleet = new Fleet(); - let ship1 = new Ship(null, "Tim"); - let ship2 = new Ship(null, "Ben"); - let part = new MissionPartConversation(new Mission(universe, fleet), [ship1, ship2], "Talk to Tim"); +import { testing } from "../../common/Testing"; +import { Fleet } from "../Fleet"; +import { Ship } from "../Ship"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionPartConversation } from "./MissionPartConversation"; - check.equals(part.title, "Talk to Tim"); - check.same(part.checkCompleted(), true, "No dialog piece"); +testing("MissionPartConversation", test => { + test.case("advances through conversation", check => { + let universe = new Universe(); + let fleet = new Fleet(); + let ship1 = new Ship(null, "Tim"); + let ship2 = new Ship(null, "Ben"); + let part = new MissionPartConversation(new Mission(universe, fleet), [ship1, ship2], "Talk to Tim"); - part.addPiece(ship1, "Hi !"); - part.addPiece(ship2, "Indeed, hi !"); - part.addPiece(null, "Hum, hello."); - check.same(part.checkCompleted(), false, "Dialog pieces added"); - check.equals(part.getCurrent(), { interlocutor: ship1, message: "Hi !" }); + check.equals(part.title, "Talk to Tim"); + check.same(part.checkCompleted(), true, "No dialog piece"); - part.next(); - check.same(part.checkCompleted(), false, "Second piece"); - check.equals(part.getCurrent(), { interlocutor: ship2, message: "Indeed, hi !" }); + part.addPiece(ship1, "Hi !"); + part.addPiece(ship2, "Indeed, hi !"); + part.addPiece(null, "Hum, hello."); + check.same(part.checkCompleted(), false, "Dialog pieces added"); + check.equals(part.getCurrent(), { interlocutor: ship1, message: "Hi !" }); - part.next(); - check.same(part.checkCompleted(), false, "Last piece"); - check.equals(part.getCurrent(), { interlocutor: null, message: "Hum, hello." }); + part.next(); + check.same(part.checkCompleted(), false, "Second piece"); + check.equals(part.getCurrent(), { interlocutor: ship2, message: "Indeed, hi !" }); - let ship = fleet.addShip(); - check.equals(part.getCurrent(), { interlocutor: ship, message: "Hum, hello." }); + part.next(); + check.same(part.checkCompleted(), false, "Last piece"); + check.equals(part.getCurrent(), { interlocutor: null, message: "Hum, hello." }); - part.next(); - check.same(part.checkCompleted(), true, "Dialog ended"); - check.equals(part.getCurrent(), { interlocutor: null, message: "" }); - }) + let ship = fleet.addShip(); + check.equals(part.getCurrent(), { interlocutor: ship, message: "Hum, hello." }); - test.case("force completes", check => { - let universe = new Universe(); - let fleet = new Fleet(); - let ship = new Ship(null, "Tim"); - let part = new MissionPartConversation(new Mission(universe, fleet), [ship]); - part.addPiece(null, "Hello !"); - part.addPiece(ship, "Hiya !"); + part.next(); + check.same(part.checkCompleted(), true, "Dialog ended"); + check.equals(part.getCurrent(), { interlocutor: null, message: "" }); + }) - check.equals(part.title, "Speak with Tim"); - check.equals(part.checkCompleted(), false); - check.equals(part.getCurrent(), { interlocutor: null, message: "Hello !" }); + test.case("force completes", check => { + let universe = new Universe(); + let fleet = new Fleet(); + let ship = new Ship(null, "Tim"); + let part = new MissionPartConversation(new Mission(universe, fleet), [ship]); + part.addPiece(null, "Hello !"); + part.addPiece(ship, "Hiya !"); - part.forceComplete(); - check.equals(part.checkCompleted(), true); - check.equals(part.getCurrent(), { interlocutor: null, message: "" }); - }); - }) -} + check.equals(part.title, "Speak with Tim"); + check.equals(part.checkCompleted(), false); + check.equals(part.getCurrent(), { interlocutor: null, message: "Hello !" }); + + part.forceComplete(); + check.equals(part.checkCompleted(), true); + check.equals(part.getCurrent(), { interlocutor: null, message: "" }); + }); +}) diff --git a/src/core/missions/MissionPartConversation.ts b/src/core/missions/MissionPartConversation.ts index 8bf7bfb..0893af7 100644 --- a/src/core/missions/MissionPartConversation.ts +++ b/src/core/missions/MissionPartConversation.ts @@ -1,100 +1,100 @@ -/// +import { Ship } from "../Ship"; +import { Mission } from "./Mission"; +import { MissionPart } from "./MissionPart"; -module TK.SpaceTac { - /** - * A single conversation piece - */ - interface ConversationPiece { - // Interlocutor (null for the player's fleet) - interlocutor: Ship | null +/** + * A single conversation piece + */ +interface ConversationPiece { + // Interlocutor (null for the player's fleet) + interlocutor: Ship | null - // Text message - message: string - } - - /** - * A mission part that triggers a conversation - */ - export class MissionPartConversation extends MissionPart { - // Other ships with which the dialog will take place - interlocutors: Ship[] - - // Pieces of dialog - pieces: ConversationPiece[] = [] - - // Current piece - current_piece = 0 - - constructor(mission: Mission, interlocutors: Ship[], directive?: string) { - super(mission, directive || `Speak with ${interlocutors[0].name}`); - - this.interlocutors = interlocutors; - } - - checkCompleted(): boolean { - return this.current_piece >= this.pieces.length; - } - - forceComplete(): void { - this.skip(); - } - - /** - * Add a piece of dialog - */ - addPiece(interlocutor: Ship | null, message: string): void { - this.pieces.push({ - interlocutor: interlocutor, - message: message - }); - } - - /** - * Go to the next dialog "screen" - * - * Returns true if there is still dialog to display. - */ - next(): boolean { - this.current_piece += 1; - return !this.checkCompleted(); - } - - /** - * Skip to the end - */ - skip() { - while (this.next()) { - } - } - - /** - * Get the current piece of dialog - */ - getCurrent(): ConversationPiece { - if (this.checkCompleted()) { - return { - interlocutor: null, - message: "" - } - } else { - let piece = this.pieces[this.current_piece]; - return { - interlocutor: piece.interlocutor || this.getFleetInterlocutor(piece), - message: piece.message - } - } - } - - /** - * Get the interlocutor from the player fleet that will say the piece - */ - private getFleetInterlocutor(piece: ConversationPiece): Ship | null { - if (this.fleet.ships.length > 0) { - // TODO Choose a ship by its personality traits - return this.fleet.ships[0]; - } else { - return null; - } - } - } + // Text message + message: string +} + +/** + * A mission part that triggers a conversation + */ +export class MissionPartConversation extends MissionPart { + // Other ships with which the dialog will take place + interlocutors: Ship[] + + // Pieces of dialog + pieces: ConversationPiece[] = [] + + // Current piece + current_piece = 0 + + constructor(mission: Mission, interlocutors: Ship[], directive?: string) { + super(mission, directive || `Speak with ${interlocutors[0].name}`); + + this.interlocutors = interlocutors; + } + + checkCompleted(): boolean { + return this.current_piece >= this.pieces.length; + } + + forceComplete(): void { + this.skip(); + } + + /** + * Add a piece of dialog + */ + addPiece(interlocutor: Ship | null, message: string): void { + this.pieces.push({ + interlocutor: interlocutor, + message: message + }); + } + + /** + * Go to the next dialog "screen" + * + * Returns true if there is still dialog to display. + */ + next(): boolean { + this.current_piece += 1; + return !this.checkCompleted(); + } + + /** + * Skip to the end + */ + skip() { + while (this.next()) { + } + } + + /** + * Get the current piece of dialog + */ + getCurrent(): ConversationPiece { + if (this.checkCompleted()) { + return { + interlocutor: null, + message: "" + } + } else { + let piece = this.pieces[this.current_piece]; + return { + interlocutor: piece.interlocutor || this.getFleetInterlocutor(piece), + message: piece.message + } + } + } + + /** + * Get the interlocutor from the player fleet that will say the piece + */ + private getFleetInterlocutor(piece: ConversationPiece): Ship | null { + if (this.fleet.ships.length > 0) { + // TODO Choose a ship by its personality traits + return this.fleet.ships[0]; + } else { + return null; + } + } } diff --git a/src/core/missions/MissionPartEscort.spec.ts b/src/core/missions/MissionPartEscort.spec.ts index 1a9af8f..1450323 100644 --- a/src/core/missions/MissionPartEscort.spec.ts +++ b/src/core/missions/MissionPartEscort.spec.ts @@ -1,68 +1,79 @@ -module TK.SpaceTac.Specs { - testing("MissionPartEscort", test => { - test.case("completes when the fleet is at location, with its escort", check => { - let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); - destination.encounter_random = new SkewedRandomGenerator([0], true); +import { SkewedRandomGenerator } from "../../common/RandomGenerator"; +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { Battle } from "../Battle"; +import { Fleet } from "../Fleet"; +import { Ship } from "../Ship"; +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { TestTools } from "../TestTools"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionPartEscort } from "./MissionPartEscort"; - let universe = new Universe(); - let fleet = new Fleet(); - let ship = new Ship(null, "Zybux"); - let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship); +testing("MissionPartEscort", test => { + test.case("completes when the fleet is at location, with its escort", check => { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.encounter_random = new SkewedRandomGenerator([0], true); - check.notcontains(fleet.ships, ship); - check.equals(part.title, "Escort Zybux to Atanax system"); - check.same(part.checkCompleted(), false, "Init location"); + let universe = new Universe(); + let fleet = new Fleet(); + let ship = new Ship(null, "Zybux"); + let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship); - part.onStarted(); - check.contains(fleet.ships, ship); + check.notcontains(fleet.ships, ship); + check.equals(part.title, "Escort Zybux to Atanax system"); + check.same(part.checkCompleted(), false, "Init location"); - fleet.setLocation(destination); - check.same(part.checkCompleted(), false, "Encounter not clear"); + part.onStarted(); + check.contains(fleet.ships, ship); - destination.clearEncounter(); - check.same(part.checkCompleted(), true, "Encouter cleared"); + fleet.setLocation(destination); + check.same(part.checkCompleted(), false, "Encounter not clear"); - fleet.setLocation(new StarLocation()); - check.same(part.checkCompleted(), false, "Went to another system"); + destination.clearEncounter(); + check.same(part.checkCompleted(), true, "Encouter cleared"); - fleet.setLocation(destination); - check.same(part.checkCompleted(), true, "Back at destination"); - check.contains(fleet.ships, ship); + fleet.setLocation(new StarLocation()); + check.same(part.checkCompleted(), false, "Went to another system"); - part.onEnded(); - check.notcontains(fleet.ships, ship); - }) + fleet.setLocation(destination); + check.same(part.checkCompleted(), true, "Back at destination"); + check.contains(fleet.ships, ship); - test.case("sets the escorted ship as critical in battles", check => { - let universe = new Universe(); - let fleet = new Fleet(); - let ship1 = fleet.addShip(); - let ship2 = fleet.addShip(); - let ship = new Ship(); - let destination = new StarLocation(new Star()); - let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship); + part.onEnded(); + check.notcontains(fleet.ships, ship); + }) - part.onStarted(); - check.contains(fleet.ships, ship); + test.case("sets the escorted ship as critical in battles", check => { + let universe = new Universe(); + let fleet = new Fleet(); + let ship1 = fleet.addShip(); + let ship2 = fleet.addShip(); + let ship = new Ship(); + let destination = new StarLocation(new Star()); + let part = new MissionPartEscort(new Mission(universe, fleet), destination, ship); - let enemy = new Fleet(); - enemy.addShip(); - let battle = new Battle(fleet, enemy); - battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); - battle.start(); - battle.performChecks(); - check.equals(battle.ended, false); + part.onStarted(); + check.contains(fleet.ships, ship); - // if a fleet member dies, it is not over - ship1.setDead(); - battle.performChecks(); - check.equals(battle.ended, false); + let enemy = new Fleet(); + enemy.addShip(); + let battle = new Battle(fleet, enemy); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); + battle.start(); + battle.performChecks(); + check.equals(battle.ended, false); - // if the critical ship dies, it is defeat - ship.setDead(); - battle.performChecks(); - check.equals(battle.ended, true); - check.notsame(nn(battle.outcome).winner, fleet.id); - }) - }) -} + // if a fleet member dies, it is not over + ship1.setDead(); + battle.performChecks(); + check.equals(battle.ended, false); + + // if the critical ship dies, it is defeat + ship.setDead(); + battle.performChecks(); + check.equals(battle.ended, true); + check.notsame(nn(battle.outcome).winner, fleet.id); + }) +}) diff --git a/src/core/missions/MissionPartEscort.ts b/src/core/missions/MissionPartEscort.ts index b32cb92..c5543de 100644 --- a/src/core/missions/MissionPartEscort.ts +++ b/src/core/missions/MissionPartEscort.ts @@ -1,29 +1,31 @@ -/// +import { contains } from "../../common/Tools"; +import { Ship } from "../Ship"; +import { StarLocation } from "../StarLocation"; +import { Mission } from "./Mission"; +import { MissionPartGoTo } from "./MissionPartGoTo"; -module TK.SpaceTac { - /** - * A mission part that requires the fleet to escort a specific ship to a destination - */ - export class MissionPartEscort extends MissionPartGoTo { - ship: Ship +/** + * A mission part that requires the fleet to escort a specific ship to a destination + */ +export class MissionPartEscort extends MissionPartGoTo { + ship: Ship - constructor(mission: Mission, destination: StarLocation, ship: Ship, directive?: string) { - super(mission, destination, directive || `Escort ${ship.name} to ${destination.star.name} system`); + constructor(mission: Mission, destination: StarLocation, ship: Ship, directive?: string) { + super(mission, destination, directive || `Escort ${ship.name} to ${destination.star.name} system`); - this.ship = ship; - } + this.ship = ship; + } - checkCompleted(): boolean { - return super.checkCompleted() && contains(this.fleet.ships, this.ship); - } + checkCompleted(): boolean { + return super.checkCompleted() && contains(this.fleet.ships, this.ship); + } - onStarted(): void { - this.ship.critical = true; - this.fleet.addShip(this.ship); - } + onStarted(): void { + this.ship.critical = true; + this.fleet.addShip(this.ship); + } - onEnded(): void { - this.fleet.removeShip(this.ship); - } - } + onEnded(): void { + this.fleet.removeShip(this.ship); + } } diff --git a/src/core/missions/MissionPartGoTo.spec.ts b/src/core/missions/MissionPartGoTo.spec.ts index 5dcc84f..0763d03 100644 --- a/src/core/missions/MissionPartGoTo.spec.ts +++ b/src/core/missions/MissionPartGoTo.spec.ts @@ -1,41 +1,48 @@ -module TK.SpaceTac.Specs { - testing("MissionPartGoTo", test => { - test.case("completes when the fleet is at location, without encounter", check => { - let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); - destination.encounter_random = new SkewedRandomGenerator([0], true); +import { SkewedRandomGenerator } from "../../common/RandomGenerator"; +import { testing } from "../../common/Testing"; +import { Fleet } from "../Fleet"; +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { Universe } from "../Universe"; +import { Mission } from "./Mission"; +import { MissionPartGoTo } from "./MissionPartGoTo"; - let universe = new Universe(); - let fleet = new Fleet(); - let part = new MissionPartGoTo(new Mission(universe, fleet), destination); +testing("MissionPartGoTo", test => { + test.case("completes when the fleet is at location, without encounter", check => { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.encounter_random = new SkewedRandomGenerator([0], true); - check.equals(part.title, "Go to Atanax system"); - check.same(part.checkCompleted(), false, "Init location"); + let universe = new Universe(); + let fleet = new Fleet(); + let part = new MissionPartGoTo(new Mission(universe, fleet), destination); - fleet.setLocation(destination); - check.same(part.checkCompleted(), false, "Encounter not clear"); + check.equals(part.title, "Go to Atanax system"); + check.same(part.checkCompleted(), false, "Init location"); - destination.clearEncounter(); - check.same(part.checkCompleted(), true, "Encouter cleared"); + fleet.setLocation(destination); + check.same(part.checkCompleted(), false, "Encounter not clear"); - fleet.setLocation(new StarLocation()); - check.same(part.checkCompleted(), false, "Went to another system"); + destination.clearEncounter(); + check.same(part.checkCompleted(), true, "Encouter cleared"); - fleet.setLocation(destination); - check.same(part.checkCompleted(), true, "Back at destination"); - }) + fleet.setLocation(new StarLocation()); + check.same(part.checkCompleted(), false, "Went to another system"); - test.case("force completes", check => { - let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); - destination.encounter_random = new SkewedRandomGenerator([0], true); + fleet.setLocation(destination); + check.same(part.checkCompleted(), true, "Back at destination"); + }) - let universe = new Universe(); - let fleet = new Fleet(); - let part = new MissionPartGoTo(new Mission(universe, fleet), destination, "Investigate"); + test.case("force completes", check => { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.encounter_random = new SkewedRandomGenerator([0], true); - check.equals(part.title, "Investigate"); - check.equals(part.checkCompleted(), false); - part.forceComplete(); - check.equals(part.checkCompleted(), true); - }); - }) -} + let universe = new Universe(); + let fleet = new Fleet(); + let part = new MissionPartGoTo(new Mission(universe, fleet), destination, "Investigate"); + + check.equals(part.title, "Investigate"); + check.equals(part.checkCompleted(), false); + part.forceComplete(); + check.equals(part.checkCompleted(), true); + }); +}) diff --git a/src/core/missions/MissionPartGoTo.ts b/src/core/missions/MissionPartGoTo.ts index d103031..2f10a42 100644 --- a/src/core/missions/MissionPartGoTo.ts +++ b/src/core/missions/MissionPartGoTo.ts @@ -1,40 +1,41 @@ -/// +import { Star } from "../Star"; +import { StarLocation } from "../StarLocation"; +import { Mission } from "./Mission"; +import { MissionPart } from "./MissionPart"; -module TK.SpaceTac { - /** - * Level of hint to help find a destination - */ - export enum MissionPartDestinationHint { - PRECISE, - SYSTEM_AND_INFO, - SYSTEM - } - - /** - * A mission part that requires the fleet to go to a specific location - */ - export class MissionPartGoTo extends MissionPart { - destination: StarLocation - hint: MissionPartDestinationHint - - constructor(mission: Mission, destination: StarLocation, directive?: string, hint = MissionPartDestinationHint.PRECISE) { - super(mission, directive || `Go to ${destination.star.name} system`); - - this.destination = destination; - this.hint = hint; - } - - checkCompleted(): boolean { - return this.destination.is(this.fleet.location) && this.destination.isClear(); - } - - forceComplete(): void { - this.destination.clearEncounter(); - this.fleet.setLocation(this.destination); - } - - getLocationHint(): Star | StarLocation | null { - return (this.hint == MissionPartDestinationHint.PRECISE) ? this.destination : this.destination.star; - } - } +/** + * Level of hint to help find a destination + */ +export enum MissionPartDestinationHint { + PRECISE, + SYSTEM_AND_INFO, + SYSTEM +} + +/** + * A mission part that requires the fleet to go to a specific location + */ +export class MissionPartGoTo extends MissionPart { + destination: StarLocation + hint: MissionPartDestinationHint + + constructor(mission: Mission, destination: StarLocation, directive?: string, hint = MissionPartDestinationHint.PRECISE) { + super(mission, directive || `Go to ${destination.star.name} system`); + + this.destination = destination; + this.hint = hint; + } + + checkCompleted(): boolean { + return this.destination.is(this.fleet.location) && this.destination.isClear(); + } + + forceComplete(): void { + this.destination.clearEncounter(); + this.fleet.setLocation(this.destination); + } + + getLocationHint(): Star | StarLocation | null { + return (this.hint == MissionPartDestinationHint.PRECISE) ? this.destination : this.destination.star; + } } diff --git a/src/core/models/ModelAvenger.ts b/src/core/models/ModelAvenger.ts index 4bc228e..8e1ed2f 100644 --- a/src/core/models/ModelAvenger.ts +++ b/src/core/models/ModelAvenger.ts @@ -1,64 +1,66 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelAvenger extends ShipModel { - constructor() { - super("avenger", "Avenger"); - } +export class ModelAvenger extends ShipModel { + constructor() { + super("avenger", "Avenger"); + } - getDescription(): string { - return "A heavy ship, dedicated to firing high precision charged shots across great distances."; - } + getDescription(): string { + return "A heavy ship, dedicated to firing high precision charged shots across great distances."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - let engine = new MoveAction("Engine", { - distance_per_power: 60, - safety_distance: 250, - }); - engine.configureCooldown(1, 1); + getLevelUpgrades(level: number): ShipUpgrade[] { + let engine = new MoveAction("Engine", { + distance_per_power: 60, + safety_distance: 250, + }); + engine.configureCooldown(1, 1); - // TODO Weapons should be less efficient in short range + // TODO Weapons should be less efficient in short range - let charged_shot = new TriggerAction("Charged Shot", { - effects: [new DamageEffect(3)], - power: 4, - range: 900, - }, "gatlinggun"); - charged_shot.configureCooldown(2, 2); + let charged_shot = new TriggerAction("Charged Shot", { + effects: [new DamageEffect(3)], + power: 4, + range: 900, + }, "gatlinggun"); + charged_shot.configureCooldown(2, 2); - let long_range_missile = new TriggerAction("Long Range Missile", { - effects: [new DamageEffect(2)], - power: 4, - range: 700, blast: 120, - }, "submunitionmissile"); - long_range_missile.configureCooldown(1, 2); + let long_range_missile = new TriggerAction("Long Range Missile", { + effects: [new DamageEffect(2)], + power: 4, + range: 700, blast: 120, + }, "submunitionmissile"); + long_range_missile.configureCooldown(1, 2); - if (level == 1) { - return [ - { - code: "Avenger Base", - effects: [ - new AttributeEffect("hull_capacity", 2), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 4), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Charged Shot", - actions: [charged_shot] - }, - { - code: "Long Range Missile", - actions: [long_range_missile] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + if (level == 1) { + return [ + { + code: "Avenger Base", + effects: [ + new AttributeEffect("hull_capacity", 2), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 4), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Charged Shot", + actions: [charged_shot] + }, + { + code: "Long Range Missile", + actions: [long_range_missile] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelBreeze.ts b/src/core/models/ModelBreeze.ts index ec88472..7c51314 100644 --- a/src/core/models/ModelBreeze.ts +++ b/src/core/models/ModelBreeze.ts @@ -1,64 +1,67 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect, DamageEffectMode } from "../effects/DamageEffect"; +import { ValueTransferEffect } from "../effects/ValueTransferEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelBreeze extends ShipModel { - constructor() { - super("breeze", "Breeze"); - } +export class ModelBreeze extends ShipModel { + constructor() { + super("breeze", "Breeze"); + } - getDescription(): string { - return "A swift piece of maneuvrability, able to go deep behind enemy lines, and come back without a scratch."; - } + getDescription(): string { + return "A swift piece of maneuvrability, able to go deep behind enemy lines, and come back without a scratch."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 460, - safety_distance: 100 - }); - engine.configureCooldown(2, 1); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 460, + safety_distance: 100 + }); + engine.configureCooldown(2, 1); - let gatling = new TriggerAction("Gatling Gun", { - effects: [new DamageEffect(2, DamageEffectMode.SHIELD_THEN_HULL)], - power: 2, - range: 200, - }, "gatlinggun"); - gatling.configureCooldown(2, 1); + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(2, DamageEffectMode.SHIELD_THEN_HULL)], + power: 2, + range: 200, + }, "gatlinggun"); + gatling.configureCooldown(2, 1); - let shield_steal = new TriggerAction("Shield Steal", { - effects: [new ValueTransferEffect("shield", -1)], - power: 1, - blast: 300 - }, "shieldtransfer"); - shield_steal.configureCooldown(1, 2); + let shield_steal = new TriggerAction("Shield Steal", { + effects: [new ValueTransferEffect("shield", -1)], + power: 1, + blast: 300 + }, "shieldtransfer"); + shield_steal.configureCooldown(1, 2); - return [ - { - code: "Breeze Base", - effects: [ - new AttributeEffect("initiative", 3), - new AttributeEffect("evasion", 1), - new AttributeEffect("hull_capacity", 1), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Gatling Gun", - actions: [gatling] - }, - { - code: "Shield Steal", - actions: [shield_steal] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Breeze Base", + effects: [ + new AttributeEffect("initiative", 3), + new AttributeEffect("evasion", 1), + new AttributeEffect("hull_capacity", 1), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Shield Steal", + actions: [shield_steal] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelCommodore.ts b/src/core/models/ModelCommodore.ts index eadae10..af2d096 100644 --- a/src/core/models/ModelCommodore.ts +++ b/src/core/models/ModelCommodore.ts @@ -1,70 +1,75 @@ -/// +import { ActionTargettingFilter } from "../actions/BaseAction"; +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { VigilanceAction } from "../actions/VigilanceAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect, DamageEffectMode } from "../effects/DamageEffect"; +import { ValueTransferEffect } from "../effects/ValueTransferEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelCommodore extends ShipModel { - constructor() { - super("commodore", "Commodore"); - } +export class ModelCommodore extends ShipModel { + constructor() { + super("commodore", "Commodore"); + } - getDescription(): string { - return "A devil whirlwind, very dangerous to surround."; - } + getDescription(): string { + return "A devil whirlwind, very dangerous to surround."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 120, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 120, + }); - let laser = new TriggerAction("Wingspan Laser", { - effects: [new DamageEffect(3, DamageEffectMode.SHIELD_THEN_HULL)], - power: 4, - range: 250, angle: 140, - }, "prokhorovlaser"); - laser.configureCooldown(3, 1); + let laser = new TriggerAction("Wingspan Laser", { + effects: [new DamageEffect(3, DamageEffectMode.SHIELD_THEN_HULL)], + power: 4, + range: 250, angle: 140, + }, "prokhorovlaser"); + laser.configureCooldown(3, 1); - let interceptors = new VigilanceAction("Interceptors Field", { radius: 200, power: 3, filter: ActionTargettingFilter.ENEMIES }, { - intruder_count: 1, - intruder_effects: [new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL)] - }, "interceptors"); + let interceptors = new VigilanceAction("Interceptors Field", { radius: 200, power: 3, filter: ActionTargettingFilter.ENEMIES }, { + intruder_count: 1, + intruder_effects: [new DamageEffect(4, DamageEffectMode.SHIELD_THEN_HULL)] + }, "interceptors"); - let power_steal = new TriggerAction("Power Thief", { - effects: [new ValueTransferEffect("power", -1)], - power: 1, - blast: 250 - }, "powerdepleter"); - power_steal.configureCooldown(1, 1); + let power_steal = new TriggerAction("Power Thief", { + effects: [new ValueTransferEffect("power", -1)], + power: 1, + blast: 250 + }, "powerdepleter"); + power_steal.configureCooldown(1, 1); - return [ - { - code: "Commodore Base", - effects: [ - new AttributeEffect("initiative", 2), - new AttributeEffect("hull_capacity", 2), - new AttributeEffect("shield_capacity", 3), - new AttributeEffect("power_capacity", 5), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Wingspan Laser", - actions: [laser] - }, - { - code: "Power Thief", - actions: [power_steal] - }, - { - code: "Interceptors Field", - actions: [interceptors] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Commodore Base", + effects: [ + new AttributeEffect("initiative", 2), + new AttributeEffect("hull_capacity", 2), + new AttributeEffect("shield_capacity", 3), + new AttributeEffect("power_capacity", 5), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Wingspan Laser", + actions: [laser] + }, + { + code: "Power Thief", + actions: [power_steal] + }, + { + code: "Interceptors Field", + actions: [interceptors] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelCreeper.ts b/src/core/models/ModelCreeper.ts index affc6e8..6825a1a 100644 --- a/src/core/models/ModelCreeper.ts +++ b/src/core/models/ModelCreeper.ts @@ -1,73 +1,79 @@ -/// +import { ActionTargettingFilter } from "../actions/BaseAction"; +import { DeployDroneAction } from "../actions/DeployDroneAction"; +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { RepelEffect } from "../effects/RepelEffect"; +import { ValueEffect } from "../effects/ValueEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelCreeper extends ShipModel { - constructor() { - super("creeper", "Creeper"); - } +export class ModelCreeper extends ShipModel { + constructor() { + super("creeper", "Creeper"); + } - getDescription(): string { - return "A fast ship, with low firepower but extensive support modules."; - } + getDescription(): string { + return "A fast ship, with low firepower but extensive support modules."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 240, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 240, + }); - let gatling = new TriggerAction("Gatling Gun", { - effects: [new DamageEffect(1)], - power: 2, - range: 200, - }, "gatlinggun"); - gatling.configureCooldown(1, 1); + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(1)], + power: 2, + range: 200, + }, "gatlinggun"); + gatling.configureCooldown(1, 1); - let repulse = new TriggerAction("Repulser", { - effects: [new RepelEffect(150)], - power: 2, - blast: 350, - }, "gravitshield"); - repulse.configureCooldown(1, 1); + let repulse = new TriggerAction("Repulser", { + effects: [new RepelEffect(150)], + power: 2, + blast: 350, + }, "gravitshield"); + repulse.configureCooldown(1, 1); - let repairdrone = new DeployDroneAction("Repair Drone", { power: 3, filter: ActionTargettingFilter.ALLIES }, { - deploy_distance: 300, - drone_radius: 150, - drone_effects: [ - new ValueEffect("hull", 0, 0, 0, 1) - ] - }, "repairdrone"); + let repairdrone = new DeployDroneAction("Repair Drone", { power: 3, filter: ActionTargettingFilter.ALLIES }, { + deploy_distance: 300, + drone_radius: 150, + drone_effects: [ + new ValueEffect("hull", 0, 0, 0, 1) + ] + }, "repairdrone"); - return [ - { - code: "Creeper Base", - effects: [ - new AttributeEffect("initiative", 3), - new AttributeEffect("hull_capacity", 2), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 5), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Gatling Gun", - actions: [gatling] - }, - { - code: "Repulser", - actions: [repulse] - }, - { - code: "Repair Drone", - actions: [repairdrone] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Creeper Base", + effects: [ + new AttributeEffect("initiative", 3), + new AttributeEffect("hull_capacity", 2), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 5), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Repulser", + actions: [repulse] + }, + { + code: "Repair Drone", + actions: [repairdrone] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelFalcon.ts b/src/core/models/ModelFalcon.ts index 3c0859d..6434469 100644 --- a/src/core/models/ModelFalcon.ts +++ b/src/core/models/ModelFalcon.ts @@ -1,61 +1,63 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelFalcon extends ShipModel { - constructor() { - super("falcon", "Falcon"); - } +export class ModelFalcon extends ShipModel { + constructor() { + super("falcon", "Falcon"); + } - getDescription(): string { - return "A ship with an efficient targetting system, allowing to hit multiple foes."; - } + getDescription(): string { + return "A ship with an efficient targetting system, allowing to hit multiple foes."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 130, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 130, + }); - let missile = new TriggerAction("SubMunition Missile", { - effects: [new DamageEffect(3)], - power: 3, - range: 250, blast: 150, - }, "submunitionmissile"); - missile.configureCooldown(2, 2); + let missile = new TriggerAction("SubMunition Missile", { + effects: [new DamageEffect(3)], + power: 3, + range: 250, blast: 150, + }, "submunitionmissile"); + missile.configureCooldown(2, 2); - // TODO targetting enemies only - let gatling = new TriggerAction("Multi-head Gatling", { - effects: [new DamageEffect(2)], - power: 2, - range: 350, blast: 150, - }, "gatlinggun"); - gatling.configureCooldown(3, 2); + // TODO targetting enemies only + let gatling = new TriggerAction("Multi-head Gatling", { + effects: [new DamageEffect(2)], + power: 2, + range: 350, blast: 150, + }, "gatlinggun"); + gatling.configureCooldown(3, 2); - return [ - { - code: "Falcon Base", - effects: [ - new AttributeEffect("hull_capacity", 3), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 4), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Submunition Missile", - actions: [missile] - }, - { - code: "Gatling Gun", - actions: [gatling] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Falcon Base", + effects: [ + new AttributeEffect("hull_capacity", 3), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 4), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Submunition Missile", + actions: [missile] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelFlea.ts b/src/core/models/ModelFlea.ts index 92ea6c2..3a93ab4 100644 --- a/src/core/models/ModelFlea.ts +++ b/src/core/models/ModelFlea.ts @@ -1,73 +1,78 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { AttributeLimitEffect } from "../effects/AttributeLimitEffect"; +import { DamageEffect, DamageEffectMode } from "../effects/DamageEffect"; +import { PinnedEffect } from "../effects/PinnedEffect"; +import { StickyEffect } from "../effects/StickyEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelFlea extends ShipModel { - constructor() { - super("flea", "Flea"); - } +export class ModelFlea extends ShipModel { + constructor() { + super("flea", "Flea"); + } - getDescription(): string { - return "An agile but weak ship, specialized in disruptive technologies."; - } + getDescription(): string { + return "An agile but weak ship, specialized in disruptive technologies."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Main Engine", { - distance_per_power: 420, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Main Engine", { + distance_per_power: 420, + }); - let depleter = new TriggerAction("Power Depleter", { - effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], - power: 2, - range: 450, - }, "powerdepleter"); - depleter.configureCooldown(1, 1); + let depleter = new TriggerAction("Power Depleter", { + effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], + power: 2, + range: 450, + }, "powerdepleter"); + depleter.configureCooldown(1, 1); - let shield_basher = new TriggerAction("Shield Basher", { - effects: [new DamageEffect(2, DamageEffectMode.SHIELD_ONLY, false)], - power: 3, - range: 300, - }, "shieldbash"); - shield_basher.configureCooldown(2, 1); + let shield_basher = new TriggerAction("Shield Basher", { + effects: [new DamageEffect(2, DamageEffectMode.SHIELD_ONLY, false)], + power: 3, + range: 300, + }, "shieldbash"); + shield_basher.configureCooldown(2, 1); - let engine_hijack = new TriggerAction("Engine Hijacking", { - effects: [new StickyEffect(new PinnedEffect(), 2)], - power: 2, - range: 400, - }, "pin"); - engine_hijack.configureCooldown(1, 2); + let engine_hijack = new TriggerAction("Engine Hijacking", { + effects: [new StickyEffect(new PinnedEffect(), 2)], + power: 2, + range: 400, + }, "pin"); + engine_hijack.configureCooldown(1, 2); - return [ - { - code: "Flea Base", - effects: [ - new AttributeEffect("initiative", 2), - new AttributeEffect("evasion", 1), - new AttributeEffect("hull_capacity", 1), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: engine.name, - actions: [engine] - }, - { - code: depleter.name, - actions: [depleter] - }, - { - code: shield_basher.name, - actions: [shield_basher] - }, - { - code: engine_hijack.name, - actions: [engine_hijack] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Flea Base", + effects: [ + new AttributeEffect("initiative", 2), + new AttributeEffect("evasion", 1), + new AttributeEffect("hull_capacity", 1), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: engine.name, + actions: [engine] + }, + { + code: depleter.name, + actions: [depleter] + }, + { + code: shield_basher.name, + actions: [shield_basher] + }, + { + code: engine_hijack.name, + actions: [engine_hijack] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelJumper.ts b/src/core/models/ModelJumper.ts index 8aaab7a..7067a32 100644 --- a/src/core/models/ModelJumper.ts +++ b/src/core/models/ModelJumper.ts @@ -1,70 +1,75 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { ToggleAction } from "../actions/ToggleAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { StickyEffect } from "../effects/StickyEffect"; +import { ValueEffect } from "../effects/ValueEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelJumper extends ShipModel { - constructor() { - super("jumper", "Jumper"); - } +export class ModelJumper extends ShipModel { + constructor() { + super("jumper", "Jumper"); + } - getDescription(): string { - return "A mid-range action ship, with support abilities."; - } + getDescription(): string { + return "A mid-range action ship, with support abilities."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 310, - safety_distance: 160, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 310, + safety_distance: 160, + }); - let missile = new TriggerAction("SubMunition Missile", { - effects: [new DamageEffect(2)], - power: 3, - range: 400, blast: 120, - }, "submunitionmissile"); + let missile = new TriggerAction("SubMunition Missile", { + effects: [new DamageEffect(2)], + power: 3, + range: 400, blast: 120, + }, "submunitionmissile"); - let protector = new TriggerAction("Damage Reductor", { - effects: [new StickyEffect(new AttributeEffect("evasion", 1), 2)], - power: 3, - range: 300, blast: 150 - }, "damageprotector"); - protector.configureCooldown(1, 3); + let protector = new TriggerAction("Damage Reductor", { + effects: [new StickyEffect(new AttributeEffect("evasion", 1), 2)], + power: 3, + range: 300, blast: 150 + }, "damageprotector"); + protector.configureCooldown(1, 3); - let hull_regrowth = new ToggleAction("Hull Regrowth", { - power: 2, - effects: [new ValueEffect("hull", 0, 0, 10)] - }, "fractalhull"); + let hull_regrowth = new ToggleAction("Hull Regrowth", { + power: 2, + effects: [new ValueEffect("hull", 0, 0, 10)] + }, "fractalhull"); - return [ - { - code: "Jumper Base", - effects: [ - new AttributeEffect("initiative", 1), - new AttributeEffect("hull_capacity", 3), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Missile", - actions: [missile] - }, - { - code: "Damage Reductor", - actions: [protector] - }, - { - code: "Hull Regrowth", - actions: [hull_regrowth] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Jumper Base", + effects: [ + new AttributeEffect("initiative", 1), + new AttributeEffect("hull_capacity", 3), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Missile", + actions: [missile] + }, + { + code: "Damage Reductor", + actions: [protector] + }, + { + code: "Hull Regrowth", + actions: [hull_regrowth] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelRhino.ts b/src/core/models/ModelRhino.ts index 7e224ef..5b07195 100644 --- a/src/core/models/ModelRhino.ts +++ b/src/core/models/ModelRhino.ts @@ -1,60 +1,62 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelRhino extends ShipModel { - constructor() { - super("rhino", "Rhino"); - } +export class ModelRhino extends ShipModel { + constructor() { + super("rhino", "Rhino"); + } - getDescription(): string { - return "A sturdy ship, able to sustain massive damage."; - } + getDescription(): string { + return "A sturdy ship, able to sustain massive damage."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 120, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 120, + }); - let gatling = new TriggerAction("Gatling Gun", { - effects: [new DamageEffect(2)], - power: 3, - range: 400, - }, "gatlinggun"); - gatling.configureCooldown(2, 2); + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(2)], + power: 3, + range: 400, + }, "gatlinggun"); + gatling.configureCooldown(2, 2); - let laser = new TriggerAction("Prokhorov Laser", { - effects: [new DamageEffect(3)], - power: 4, - range: 250, angle: 60, - }, "prokhorovlaser"); + let laser = new TriggerAction("Prokhorov Laser", { + effects: [new DamageEffect(3)], + power: 4, + range: 250, angle: 60, + }, "prokhorovlaser"); - return [ - { - code: "Rhino Base", - effects: [ - new AttributeEffect("initiative", 1), - new AttributeEffect("hull_capacity", 3), - new AttributeEffect("shield_capacity", 3), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Gatling Gun", - actions: [gatling] - }, - { - code: "Prokhorov Laser", - actions: [laser] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Rhino Base", + effects: [ + new AttributeEffect("initiative", 1), + new AttributeEffect("hull_capacity", 3), + new AttributeEffect("shield_capacity", 3), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Prokhorov Laser", + actions: [laser] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelTomahawk.ts b/src/core/models/ModelTomahawk.ts index ad849e9..11e60d1 100644 --- a/src/core/models/ModelTomahawk.ts +++ b/src/core/models/ModelTomahawk.ts @@ -1,79 +1,82 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { CooldownEffect } from "../effects/CooldownEffect"; +import { DamageEffect } from "../effects/DamageEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelTomahawk extends ShipModel { - constructor() { - super("tomahawk", "Tomahawk"); - } +export class ModelTomahawk extends ShipModel { + constructor() { + super("tomahawk", "Tomahawk"); + } - getDescription(): string { - return "A ship compensating its somewhat weak equipments with high power and usability."; - } + getDescription(): string { + return "A ship compensating its somewhat weak equipments with high power and usability."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 120, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 120, + }); - let gatling1 = new TriggerAction("Primary Gatling", { - effects: [new DamageEffect(3)], - power: 2, range: 400 - }, "gatlinggun"); - gatling1.configureCooldown(1, 2); + let gatling1 = new TriggerAction("Primary Gatling", { + effects: [new DamageEffect(3)], + power: 2, range: 400 + }, "gatlinggun"); + gatling1.configureCooldown(1, 2); - let gatling2 = new TriggerAction("Secondary Gatling", { - effects: [new DamageEffect(2)], - power: 1, range: 200 - }, "gatlinggun"); - gatling2.configureCooldown(1, 2); + let gatling2 = new TriggerAction("Secondary Gatling", { + effects: [new DamageEffect(2)], + power: 1, range: 200 + }, "gatlinggun"); + gatling2.configureCooldown(1, 2); - let missile = new TriggerAction("Diffuse Missiles", { - effects: [new DamageEffect(2)], - power: 2, - range: 200, blast: 100, - }, "submunitionmissile"); - missile.configureCooldown(1, 2); + let missile = new TriggerAction("Diffuse Missiles", { + effects: [new DamageEffect(2)], + power: 2, + range: 200, blast: 100, + }, "submunitionmissile"); + missile.configureCooldown(1, 2); - let cooler = new TriggerAction("Circuits Cooler", { - effects: [new CooldownEffect(1, 1)], - power: 1, - }, "kelvingenerator"); + let cooler = new TriggerAction("Circuits Cooler", { + effects: [new CooldownEffect(1, 1)], + power: 1, + }, "kelvingenerator"); - return [ - { - code: "Tomahawk Base", - effects: [ - new AttributeEffect("initiative", 2), - new AttributeEffect("hull_capacity", 2), - new AttributeEffect("shield_capacity", 1), - new AttributeEffect("power_capacity", 5), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Primary Gatling", - actions: [gatling1] - }, - { - code: "Secondary Gatling", - actions: [gatling2] - }, - { - code: "SubMunition Missile", - actions: [missile] - }, - { - code: "Cooler", - actions: [cooler] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Tomahawk Base", + effects: [ + new AttributeEffect("initiative", 2), + new AttributeEffect("hull_capacity", 2), + new AttributeEffect("shield_capacity", 1), + new AttributeEffect("power_capacity", 5), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Primary Gatling", + actions: [gatling1] + }, + { + code: "Secondary Gatling", + actions: [gatling2] + }, + { + code: "SubMunition Missile", + actions: [missile] + }, + { + code: "Cooler", + actions: [cooler] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelTrapper.ts b/src/core/models/ModelTrapper.ts index 17ef046..cf2cd1e 100644 --- a/src/core/models/ModelTrapper.ts +++ b/src/core/models/ModelTrapper.ts @@ -1,72 +1,78 @@ -/// +import { ActionTargettingFilter } from "../actions/BaseAction"; +import { MoveAction } from "../actions/MoveAction"; +import { ToggleAction } from "../actions/ToggleAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { AttributeLimitEffect } from "../effects/AttributeLimitEffect"; +import { DamageEffect, DamageEffectMode } from "../effects/DamageEffect"; +import { StickyEffect } from "../effects/StickyEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelTrapper extends ShipModel { - constructor() { - super("trapper", "Trapper"); - } +export class ModelTrapper extends ShipModel { + constructor() { + super("trapper", "Trapper"); + } - getDescription(): string { - return "A mostly defensive ship, used to protect allies from enemy fire."; - } + getDescription(): string { + return "A mostly defensive ship, used to protect allies from enemy fire."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 220, - }); - engine.configureCooldown(1, 1); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 220, + }); + engine.configureCooldown(1, 1); - let protector = new ToggleAction("Damage Protector", { - power: 4, - radius: 300, - effects: [new AttributeEffect("evasion", 1)], - filter: ActionTargettingFilter.ALLIES - }); + let protector = new ToggleAction("Damage Protector", { + power: 4, + radius: 300, + effects: [new AttributeEffect("evasion", 1)], + filter: ActionTargettingFilter.ALLIES + }); - let depleter = new TriggerAction("Power Depleter", { - effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], - power: 2, - range: 200, - }, "powerdepleter"); - depleter.configureCooldown(1, 1); + let depleter = new TriggerAction("Power Depleter", { + effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], + power: 2, + range: 200, + }, "powerdepleter"); + depleter.configureCooldown(1, 1); - let missile = new TriggerAction("Defense Missiles", { - effects: [new DamageEffect(3, DamageEffectMode.SHIELD_THEN_HULL)], - power: 3, - range: 200, blast: 180, - }, "submunitionmissile"); + let missile = new TriggerAction("Defense Missiles", { + effects: [new DamageEffect(3, DamageEffectMode.SHIELD_THEN_HULL)], + power: 3, + range: 200, blast: 180, + }, "submunitionmissile"); - return [ - { - code: "Trapper Base", - effects: [ - new AttributeEffect("evasion", 1), - new AttributeEffect("hull_capacity", 1), - new AttributeEffect("shield_capacity", 2), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Damage Protector", - actions: [protector] - }, - { - code: "Power Depleter", - actions: [depleter] - }, - { - code: "SubMunition Missile", - actions: [missile] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Trapper Base", + effects: [ + new AttributeEffect("evasion", 1), + new AttributeEffect("hull_capacity", 1), + new AttributeEffect("shield_capacity", 2), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Damage Protector", + actions: [protector] + }, + { + code: "Power Depleter", + actions: [depleter] + }, + { + code: "SubMunition Missile", + actions: [missile] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ModelXander.ts b/src/core/models/ModelXander.ts index 9017730..12b9de8 100644 --- a/src/core/models/ModelXander.ts +++ b/src/core/models/ModelXander.ts @@ -1,70 +1,73 @@ -/// +import { MoveAction } from "../actions/MoveAction"; +import { TriggerAction } from "../actions/TriggerAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { DamageEffect, DamageEffectMode } from "../effects/DamageEffect"; +import { ValueEffect } from "../effects/ValueEffect"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; -module TK.SpaceTac { - export class ModelXander extends ShipModel { - constructor() { - super("xander", "Xander"); - } +export class ModelXander extends ShipModel { + constructor() { + super("xander", "Xander"); + } - getDescription(): string { - return "A ship with impressive survival capabilities."; - } + getDescription(): string { + return "A ship with impressive survival capabilities."; + } - getLevelUpgrades(level: number): ShipUpgrade[] { - if (level == 1) { - let engine = new MoveAction("Engine", { - distance_per_power: 280, - }); + getLevelUpgrades(level: number): ShipUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 280, + }); - let laser = new TriggerAction("Prokhorov Laser", { - effects: [new DamageEffect(2, DamageEffectMode.SHIELD_THEN_HULL)], - power: 3, - range: 250, angle: 80, - }); + let laser = new TriggerAction("Prokhorov Laser", { + effects: [new DamageEffect(2, DamageEffectMode.SHIELD_THEN_HULL)], + power: 3, + range: 250, angle: 80, + }); - let hull = new TriggerAction("Hull Shedding", { - effects: [new ValueEffect("hull", 2)], - power: 1 - }, "fractalhull"); - hull.configureCooldown(1, 4); + let hull = new TriggerAction("Hull Shedding", { + effects: [new ValueEffect("hull", 2)], + power: 1 + }, "fractalhull"); + hull.configureCooldown(1, 4); - let disengage = new MoveAction("Disengage", { - distance_per_power: 1000, - safety_distance: 200, - }, "ionthruster"); - disengage.configureCooldown(1, 3); + let disengage = new MoveAction("Disengage", { + distance_per_power: 1000, + safety_distance: 200, + }, "ionthruster"); + disengage.configureCooldown(1, 3); - return [ - { - code: "Xander Base", - effects: [ - new AttributeEffect("initiative", 1), - new AttributeEffect("evasion", 2), - new AttributeEffect("hull_capacity", 2), - new AttributeEffect("shield_capacity", 1), - new AttributeEffect("power_capacity", 6), - ] - }, - { - code: "Main Engine", - actions: [engine] - }, - { - code: "Prokhorov Laser", - actions: [laser] - }, - { - code: "Fractal Hull", - actions: [hull] - }, - { - code: "Disengage", - actions: [disengage] - }, - ]; - } else { - return this.getStandardUpgrades(level); - } - } + return [ + { + code: "Xander Base", + effects: [ + new AttributeEffect("initiative", 1), + new AttributeEffect("evasion", 2), + new AttributeEffect("hull_capacity", 2), + new AttributeEffect("shield_capacity", 1), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Prokhorov Laser", + actions: [laser] + }, + { + code: "Fractal Hull", + actions: [hull] + }, + { + code: "Disengage", + actions: [disengage] + }, + ]; + } else { + return this.getStandardUpgrades(level); } + } } diff --git a/src/core/models/ShipModel.spec.ts b/src/core/models/ShipModel.spec.ts index 63242d2..2c19c2f 100644 --- a/src/core/models/ShipModel.spec.ts +++ b/src/core/models/ShipModel.spec.ts @@ -1,58 +1,60 @@ -module TK.SpaceTac.Specs { - testing("ShipModel", test => { - test.case("picks random models from default collection", check => { - check.patch(console, "error", null); - check.patch(ShipModel, "getDefaultCollection", nnf([], iterator([ - [new ShipModel("a")], - [], - [new ShipModel("a"), new ShipModel("b")], - [new ShipModel("a")], - ]))); +import { testing } from "../../common/Testing"; +import { cmp, iterator, nnf, sorted } from "../../common/Tools"; +import { ShipModel, ShipUpgrade } from "./ShipModel"; - check.equals(ShipModel.getRandomModel(), new ShipModel("a"), "pick from a one-item list"); - check.equals(ShipModel.getRandomModel(), new ShipModel(), "pick from an empty list"); +testing("ShipModel", test => { + test.case("picks random models from default collection", check => { + check.patch(console, "error", null); + check.patch(ShipModel, "getDefaultCollection", nnf([], iterator([ + [new ShipModel("a")], + [], + [new ShipModel("a"), new ShipModel("b")], + [new ShipModel("a")], + ]))); - check.equals(sorted(ShipModel.getRandomModels(2), (a, b) => cmp(a.code, b.code)), [new ShipModel("a"), new ShipModel("b")], "sample from good-sized list"); - check.equals(ShipModel.getRandomModels(2), [new ShipModel("a"), new ShipModel("a")], "sample from too small list"); - check.equals(ShipModel.getRandomModels(2), [new ShipModel(), new ShipModel()], "sample from empty list"); - }); + check.equals(ShipModel.getRandomModel(), new ShipModel("a"), "pick from a one-item list"); + check.equals(ShipModel.getRandomModel(), new ShipModel(), "pick from an empty list"); - test.case("makes upgrades available by level", check => { - let model = new ShipModel(); + check.equals(sorted(ShipModel.getRandomModels(2), (a, b) => cmp(a.code, b.code)), [new ShipModel("a"), new ShipModel("b")], "sample from good-sized list"); + check.equals(ShipModel.getRandomModels(2), [new ShipModel("a"), new ShipModel("a")], "sample from too small list"); + check.equals(ShipModel.getRandomModels(2), [new ShipModel(), new ShipModel()], "sample from empty list"); + }); - function verify(desc: string, level: number, specific: string[], available: string[], activated: string[], chosen: string[] = []) { - check.in(`${desc} level ${level}`, check => { - check.equals(model.getLevelUpgrades(level).map(u => u.code), specific, "specific"); - check.equals(model.getAvailableUpgrades(level).map(u => u.code), available, "available"); - check.equals(model.getActivatedUpgrades(level, chosen).map(u => u.code), activated, "activated"); - }); - } + test.case("makes upgrades available by level", check => { + let model = new ShipModel(); - verify("initial", 1, [], [], []); + function verify(desc: string, level: number, specific: string[], available: string[], activated: string[], chosen: string[] = []) { + check.in(`${desc} level ${level}`, check => { + check.equals(model.getLevelUpgrades(level).map(u => u.code), specific, "specific"); + check.equals(model.getAvailableUpgrades(level).map(u => u.code), available, "available"); + check.equals(model.getActivatedUpgrades(level, chosen).map(u => u.code), activated, "activated"); + }); + } - check.patch(model, "getLevelUpgrades", (level: number): ShipUpgrade[] => { - if (level == 1) { - return [ - { code: "l1" }, - ]; - } else if (level == 2) { - return [ - { code: "l2a" }, - { code: "l2b" } - ]; - } else { - return []; - } - }); + verify("initial", 1, [], [], []); - verify("standard", 0, [], [], []); - verify("standard", 1, ["l1"], ["l1"], ["l1"]); - verify("standard", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1"]); - verify("standard", 3, [], ["l1", "l2a", "l2b"], ["l1"]); - - verify("with actives", 1, ["l1"], ["l1"], ["l1"], ["l2a", "l666"]); - verify("with actives", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); - verify("with actives", 3, [], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); - }); + check.patch(model, "getLevelUpgrades", (level: number): ShipUpgrade[] => { + if (level == 1) { + return [ + { code: "l1" }, + ]; + } else if (level == 2) { + return [ + { code: "l2a" }, + { code: "l2b" } + ]; + } else { + return []; + } }); -} + + verify("standard", 0, [], [], []); + verify("standard", 1, ["l1"], ["l1"], ["l1"]); + verify("standard", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1"]); + verify("standard", 3, [], ["l1", "l2a", "l2b"], ["l1"]); + + verify("with actives", 1, ["l1"], ["l1"], ["l1"], ["l2a", "l666"]); + verify("with actives", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); + verify("with actives", 3, [], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); + }); +}); diff --git a/src/core/models/ShipModel.ts b/src/core/models/ShipModel.ts index b07b66b..e30a1df 100644 --- a/src/core/models/ShipModel.ts +++ b/src/core/models/ShipModel.ts @@ -1,184 +1,189 @@ -module TK.SpaceTac { - /** - * Single upgrade for a ship - * - * Upgrades allow for customizing a model, and are unlocked at given levels - */ - export type ShipUpgrade = { - // Displayable upgrade name, should be unique on the model - code: string - // Upgrade points cost (may be used to balance upgrades) - cost?: number - // Textual description of the upgrade - description?: string - // Optional list of upgrade codes that must be activated for this one to be available - depends?: string[] - // Optional list of upgrade codes that this upgrade will fully replace - replaces?: string[] - // Optional list of upgrade codes that conflicts with this upgrade - conflicts?: string[] - // List of actions that this upgrade offers - actions?: BaseAction[] - // List of effects that this upgrade offers - effects?: BaseEffect[] - } +import { NAMESPACE } from "../.."; +import { RandomGenerator } from "../../common/RandomGenerator"; +import { contains, flatten, range } from "../../common/Tools"; +import { BaseAction } from "../actions/BaseAction"; +import { AttributeEffect } from "../effects/AttributeEffect"; +import { BaseEffect } from "../effects/BaseEffect"; - /** - * Base class for ship models. - * - * A model defines the ship's design, actions, permanent effects, and levelling options. - */ - export class ShipModel { - constructor( - // Code to identify the model - readonly code = "default", - // Human-readable model name - readonly name = "Ship" - ) { } - - /** - * Check if this model is available at a given level - */ - isAvailable(level: number): boolean { - // TODO - return true; - } - - /** - * Get a textual description of the model - */ - getDescription(): string { - return ""; - } - - /** - * Get basic level upgrades - */ - protected getStandardUpgrades(level: number): ShipUpgrade[] { - return [ - { code: `Level ${level} Hull Upgrade`, effects: [new AttributeEffect("hull_capacity", 1)], cost: 3 }, - { code: `Level ${level} Shield Upgrade`, effects: [new AttributeEffect("shield_capacity", 1)], cost: 3 }, - { code: `Level ${level} Power Upgrade`, effects: [new AttributeEffect("power_capacity", 1)], cost: 3 }, - ]; - } - - /** - * Get the list of upgrades unlocked at a given level - */ - getLevelUpgrades(level: number): ShipUpgrade[] { - return []; - } - - /** - * Get the list of upgrades activated, given a ship level and an upgrade set - */ - getActivatedUpgrades(level: number, upgrade_codes: string[]): ShipUpgrade[] { - let result: ShipUpgrade[] = []; - - range(level).forEach(i => { - let upgrades = this.getLevelUpgrades(i + 1); - if (i == 0) { - result = result.concat(upgrades); - } else { - // TODO Apply depends, replaces and conflicts - upgrades.forEach(upgrade => { - if (contains(upgrade_codes, upgrade.code)) { - result.push(upgrade); - } - }); - } - }); - - return result; - } - - /** - * Get the list of available upgrades, given a ship level - * - * This does not filter the upgrades on dependencies - */ - getAvailableUpgrades(level: number): ShipUpgrade[] { - return flatten(range(level).map(i => this.getLevelUpgrades(i + 1))); - } - - /** - * Get the list of actions at a given level and upgrades set - * - * This does not include an "end turn" action. - */ - getActions(level: number, upgrade_codes: string[]): BaseAction[] { - return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.actions || [])); - } - - /** - * Get the list of permanent effects at a given level and upgrades set - */ - getEffects(level: number, upgrade_codes: string[]): BaseEffect[] { - return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.effects || [])); - } - - /** - * Get the default ship model collection available in-game - * - * This scans the current namespace for model classes starting with 'Model'. - */ - static getDefaultCollection(): ShipModel[] { - let result: ShipModel[] = []; - let namespace: any = TK.SpaceTac; - - for (let class_name in namespace) { - if (class_name && class_name.indexOf("Model") == 0) { - let model_class = namespace[class_name]; - if (model_class.prototype instanceof ShipModel) { - let model = new model_class(); - result.push(model); - } - } - } - - return result; - } - - /** - * Pick a random model in the default collection - */ - static getRandomModel(level?: number, random = RandomGenerator.global): ShipModel { - let collection = ShipModel.getDefaultCollection(); - if (level) { - collection = collection.filter(model => model.isAvailable(level)); - } - - if (collection.length == 0) { - console.error("Couldn't pick a random ship model"); - return new ShipModel(); - } else { - return random.choice(collection); - } - } - - /** - * Pick random models in the default collection - * - * At first it tries to pick unique models, then fill with duplicates - */ - static getRandomModels(count: number, level?: number, random = RandomGenerator.global): ShipModel[] { - let collection = ShipModel.getDefaultCollection(); - if (level) { - collection = collection.filter(model => model.isAvailable(level)); - } - - if (collection.length == 0) { - console.error("Couldn't pick a random model"); - return range(count).map(() => new ShipModel()); - } else { - let result: ShipModel[] = []; - while (count > 0) { - let picked = random.sample(collection, Math.min(count, collection.length)); - result = result.concat(picked); - count -= picked.length; - } - return result; - } - } - } +/** + * Single upgrade for a ship + * + * Upgrades allow for customizing a model, and are unlocked at given levels + */ +export type ShipUpgrade = { + // Displayable upgrade name, should be unique on the model + code: string + // Upgrade points cost (may be used to balance upgrades) + cost?: number + // Textual description of the upgrade + description?: string + // Optional list of upgrade codes that must be activated for this one to be available + depends?: string[] + // Optional list of upgrade codes that this upgrade will fully replace + replaces?: string[] + // Optional list of upgrade codes that conflicts with this upgrade + conflicts?: string[] + // List of actions that this upgrade offers + actions?: BaseAction[] + // List of effects that this upgrade offers + effects?: BaseEffect[] +} + +/** + * Base class for ship models. + * + * A model defines the ship's design, actions, permanent effects, and levelling options. + */ +export class ShipModel { + constructor( + // Code to identify the model + readonly code = "default", + // Human-readable model name + readonly name = "Ship" + ) { } + + /** + * Check if this model is available at a given level + */ + isAvailable(level: number): boolean { + // TODO + return true; + } + + /** + * Get a textual description of the model + */ + getDescription(): string { + return ""; + } + + /** + * Get basic level upgrades + */ + protected getStandardUpgrades(level: number): ShipUpgrade[] { + return [ + { code: `Level ${level} Hull Upgrade`, effects: [new AttributeEffect("hull_capacity", 1)], cost: 3 }, + { code: `Level ${level} Shield Upgrade`, effects: [new AttributeEffect("shield_capacity", 1)], cost: 3 }, + { code: `Level ${level} Power Upgrade`, effects: [new AttributeEffect("power_capacity", 1)], cost: 3 }, + ]; + } + + /** + * Get the list of upgrades unlocked at a given level + */ + getLevelUpgrades(level: number): ShipUpgrade[] { + return []; + } + + /** + * Get the list of upgrades activated, given a ship level and an upgrade set + */ + getActivatedUpgrades(level: number, upgrade_codes: string[]): ShipUpgrade[] { + let result: ShipUpgrade[] = []; + + range(level).forEach(i => { + let upgrades = this.getLevelUpgrades(i + 1); + if (i == 0) { + result = result.concat(upgrades); + } else { + // TODO Apply depends, replaces and conflicts + upgrades.forEach(upgrade => { + if (contains(upgrade_codes, upgrade.code)) { + result.push(upgrade); + } + }); + } + }); + + return result; + } + + /** + * Get the list of available upgrades, given a ship level + * + * This does not filter the upgrades on dependencies + */ + getAvailableUpgrades(level: number): ShipUpgrade[] { + return flatten(range(level).map(i => this.getLevelUpgrades(i + 1))); + } + + /** + * Get the list of actions at a given level and upgrades set + * + * This does not include an "end turn" action. + */ + getActions(level: number, upgrade_codes: string[]): BaseAction[] { + return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.actions || [])); + } + + /** + * Get the list of permanent effects at a given level and upgrades set + */ + getEffects(level: number, upgrade_codes: string[]): BaseEffect[] { + return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.effects || [])); + } + + /** + * Get the default ship model collection available in-game + * + * This scans the current namespace for model classes starting with 'Model'. + */ + static getDefaultCollection(): ShipModel[] { + let result: ShipModel[] = []; + let namespace = NAMESPACE; + + for (let class_name in namespace) { + if (class_name && class_name.indexOf("Model") == 0) { + let model_class = namespace[class_name] as any; + if (model_class.prototype instanceof ShipModel) { + let model = new model_class(); + result.push(model); + } + } + } + + return result; + } + + /** + * Pick a random model in the default collection + */ + static getRandomModel(level?: number, random = RandomGenerator.global): ShipModel { + let collection = ShipModel.getDefaultCollection(); + if (level) { + collection = collection.filter(model => model.isAvailable(level)); + } + + if (collection.length == 0) { + console.error("Couldn't pick a random ship model"); + return new ShipModel(); + } else { + return random.choice(collection); + } + } + + /** + * Pick random models in the default collection + * + * At first it tries to pick unique models, then fill with duplicates + */ + static getRandomModels(count: number, level?: number, random = RandomGenerator.global): ShipModel[] { + let collection = ShipModel.getDefaultCollection(); + if (level) { + collection = collection.filter(model => model.isAvailable(level)); + } + + if (collection.length == 0) { + console.error("Couldn't pick a random model"); + return range(count).map(() => new ShipModel()); + } else { + let result: ShipModel[] = []; + while (count > 0) { + let picked = random.sample(collection, Math.min(count, collection.length)); + result = result.concat(picked); + count -= picked.length; + } + return result; + } + } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c73b351 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +import { MainUI } from "./MainUI"; + +export const NAMESPACE: { [name: string]: (...args: any) => any } = { +} + +export function setupUI() { + new MainUI(); +} diff --git a/src/lib/parse.d.ts b/src/lib/parse.d.ts deleted file mode 100644 index 34db0e8..0000000 --- a/src/lib/parse.d.ts +++ /dev/null @@ -1,1100 +0,0 @@ -// Type definitions for parse v1.9.2 -// Project: https://parse.com/ -// Definitions by: Ullisen Media Group , David Poetzsch-Heffter -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -declare namespace Parse { - - var applicationId: string; - var javaScriptKey: string | undefined; - var masterKey: string | undefined; - var serverURL: string; - var VERSION: string; - - interface SuccessOption { - success?: Function; - } - - interface ErrorOption { - error?: Function; - } - - interface SuccessFailureOptions extends SuccessOption, ErrorOption { - } - - interface SessionTokenOption { - sessionToken?: string; - } - - interface WaitOption { - /** - * Set to true to wait for the server to confirm success - * before triggering an event. - */ - wait?: boolean; - } - - interface UseMasterKeyOption { - /** - * In Cloud Code and Node only, causes the Master Key to be used for this request. - */ - useMasterKey?: boolean; - } - - interface ScopeOptions extends SessionTokenOption, UseMasterKeyOption { - } - - interface SilentOption { - /** - * Set to true to avoid firing the event. - */ - silent?: boolean; - } - - /** - * A Promise is returned by async methods as a hook to provide callbacks to be - * called when the async task is fulfilled. - * - *

Typical usage would be like:

-     *    query.find().then(function(results) {
-     *      results[0].set("foo", "bar");
-     *      return results[0].saveAsync();
-     *    }).then(function(result) {
-     *      console.log("Updated " + result.id);
-     *    });
-     * 

- * - * @see Parse.Promise.prototype.then - * @class - */ - - interface IPromise { - - then(resolvedCallback: (...values: T[]) => IPromise, rejectedCallback?: (reason: any) => IPromise): IPromise; - then(resolvedCallback: (...values: T[]) => U, rejectedCallback?: (reason: any) => IPromise): IPromise; - then(resolvedCallback: (...values: T[]) => U, rejectedCallback?: (reason: any) => U): IPromise; - } - - class Promise implements IPromise { - - static as(resolvedValue: U): Promise; - static error(error: any): Promise; - static is(possiblePromise: any): Boolean; - static when(promises: IPromise[]): Promise; - static when(...promises: IPromise[]): Promise; - - always(callback: Function): Promise; - done(callback: Function): Promise; - fail(callback: Function): Promise; - reject(error: any): void; - resolve(result: any): void; - then(resolvedCallback: (...values: T[]) => IPromise, - rejectedCallback?: (reason: any) => IPromise): IPromise; - then(resolvedCallback: (...values: T[]) => U, - rejectedCallback?: (reason: any) => IPromise): IPromise; - then(resolvedCallback: (...values: T[]) => U, - rejectedCallback?: (reason: any) => U): IPromise; - } - - interface IBaseObject { - toJSON(): any; - } - - class BaseObject implements IBaseObject { - toJSON(): any; - } - - /** - * Creates a new ACL. - * If no argument is given, the ACL has no permissions for anyone. - * If the argument is a Parse.User, the ACL will have read and write - * permission for only that user. - * If the argument is any other JSON object, that object will be interpretted - * as a serialized ACL created with toJSON(). - * @see Parse.Object#setACL - * @class - * - *

An ACL, or Access Control List can be added to any - * Parse.Object to restrict access to only a subset of users - * of your application.

- */ - class ACL extends BaseObject { - - permissionsById: any; - - constructor(arg1?: any); - - setPublicReadAccess(allowed: boolean): void; - getPublicReadAccess(): boolean; - - setPublicWriteAccess(allowed: boolean): void; - getPublicWriteAccess(): boolean; - - setReadAccess(userId: User, allowed: boolean): void; - getReadAccess(userId: User): boolean; - - setReadAccess(userId: string, allowed: boolean): void; - getReadAccess(userId: string): boolean; - - setRoleReadAccess(role: Role, allowed: boolean): void; - setRoleReadAccess(role: string, allowed: boolean): void; - getRoleReadAccess(role: Role): boolean; - getRoleReadAccess(role: string): boolean; - - setRoleWriteAccess(role: Role, allowed: boolean): void; - setRoleWriteAccess(role: string, allowed: boolean): void; - getRoleWriteAccess(role: Role): boolean; - getRoleWriteAccess(role: string): boolean; - - setWriteAccess(userId: User, allowed: boolean): void; - setWriteAccess(userId: string, allowed: boolean): void; - getWriteAccess(userId: User): boolean; - getWriteAccess(userId: string): boolean; - } - - - /** - * A Parse.File is a local representation of a file that is saved to the Parse - * cloud. - * @class - * @param name {String} The file's name. This will be prefixed by a unique - * value once the file has finished saving. The file name must begin with - * an alphanumeric character, and consist of alphanumeric characters, - * periods, spaces, underscores, or dashes. - * @param data {Array} The data for the file, as either: - * 1. an Array of byte value Numbers, or - * 2. an Object like { base64: "..." } with a base64-encoded String. - * 3. a File object selected with a file upload control. (3) only works - * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+. - * For example:
-     * var fileUploadControl = $("#profilePhotoFileUpload")[0];
-     * if (fileUploadControl.files.length > 0) {
-     *   var file = fileUploadControl.files[0];
-     *   var name = "photo.jpg";
-     *   var parseFile = new Parse.File(name, file);
-     *   parseFile.save().then(function() {
-     *     // The file has been saved to Parse.
-     *   }, function(error) {
-     *     // The file either could not be read, or could not be saved to Parse.
-     *   });
-     * }
- * @param type {String} Optional Content-Type header to use for the file. If - * this is omitted, the content type will be inferred from the name's - * extension. - */ - class File { - - constructor(name: string, data: any, type?: string); - name(): string; - url(): string; - save(options?: SuccessFailureOptions): Promise; - - } - - /** - * Creates a new GeoPoint with any of the following forms:
- *
-     *   new GeoPoint(otherGeoPoint)
-     *   new GeoPoint(30, 30)
-     *   new GeoPoint([30, 30])
-     *   new GeoPoint({latitude: 30, longitude: 30})
-     *   new GeoPoint()  // defaults to (0, 0)
-     *   
- * @class - * - *

Represents a latitude / longitude point that may be associated - * with a key in a ParseObject or used as a reference point for geo queries. - * This allows proximity-based queries on the key.

- * - *

Only one key in a class may contain a GeoPoint.

- * - *

Example:

-     *   var point = new Parse.GeoPoint(30.0, -20.0);
-     *   var object = new Parse.Object("PlaceObject");
-     *   object.set("location", point);
-     *   object.save();

- */ - class GeoPoint extends BaseObject { - - latitude: number; - longitude: number; - - constructor(arg1?: any, arg2?: any); - - current(options?: SuccessFailureOptions): GeoPoint; - radiansTo(point: GeoPoint): number; - kilometersTo(point: GeoPoint): number; - milesTo(point: GeoPoint): number; - } - - /** - * History serves as a global router (per frame) to handle hashchange - * events or pushState, match the appropriate route, and trigger - * callbacks. You shouldn't ever have to create one of these yourself - * — you should use the reference to Parse.history - * that will be created for you automatically if you make use of - * Routers with routes. - * @class - * - *

A fork of Backbone.History, provided for your convenience. If you - * use this class, you must also include jQuery, or another library - * that provides a jQuery-compatible $ function. For more information, - * see the - * Backbone documentation.

- *

Available in the client SDK only.

- */ - class History { - - handlers: any[]; - interval: number; - fragment: string; - - checkUrl(e?: any): void; - getFragment(fragment?: string, forcePushState?: boolean): string; - getHash(windowOverride: Window): string; - loadUrl(fragmentOverride: any): boolean; - navigate(fragment: string, options?: any): any; - route(route: any, callback: Function): void; - start(options: any): boolean; - stop(): void; - } - - /** - * A class that is used to access all of the children of a many-to-many relationship. - * Each instance of Parse.Relation is associated with a particular parent object and key. - */ - class Relation extends BaseObject { - - parent: Object; - key: string; - targetClassName: string; - - constructor(parent?: Object, key?: string); - - //Adds a Parse.Object or an array of Parse.Objects to the relation. - add(object: Object): void; - - // Returns a Parse.Query that is limited to objects in this relation. - query(): Query; - - // Removes a Parse.Object or an array of Parse.Objects from this relation. - remove(object: Object): void; - } - - /** - * Creates a new model with defined attributes. A client id (cid) is - * automatically generated and assigned for you. - * - *

You won't normally call this method directly. It is recommended that - * you use a subclass of Parse.Object instead, created by calling - * extend.

- * - *

However, if you don't want to use a subclass, or aren't sure which - * subclass is appropriate, you can use this form:

-     *     var object = new Parse.Object("ClassName");
-     * 
- * That is basically equivalent to:
-     *     var MyClass = Parse.Object.extend("ClassName");
-     *     var object = new MyClass();
-     * 

- * - * @param {Object} attributes The initial set of data to store in the object. - * @param {Object} options A set of Backbone-like options for creating the - * object. The only option currently supported is "collection". - * @see Parse.Object.extend - * - * @class - * - *

The fundamental unit of Parse data, which implements the Backbone Model - * interface.

- */ - class Object extends BaseObject { - - id: string; - createdAt: Date; - updatedAt: Date; - attributes: any; - cid: string; - changed: boolean; - className: string; - - constructor(className?: string, options?: any); - constructor(attributes?: string[], options?: any); - - static extend(className: string, protoProps?: any, classProps?: any): any; - static fetchAll(list: T[], options: SuccessFailureOptions): Promise; - static fetchAllIfNeeded(list: T[], options: SuccessFailureOptions): Promise; - static destroyAll(list: T[], options?: Object.DestroyAllOptions): Promise; - static saveAll(list: T[], options?: Object.SaveAllOptions): Promise; - - static registerSubclass(className: string, clazz: new (options?: any) => T): void; - - initialize(): void; - add(attr: string, item: any): this; - addUnique(attr: string, item: any): any; - change(options: any): this; - changedAttributes(diff: any): boolean; - clear(options: any): any; - clone(): this; - destroy(options?: Object.DestroyOptions): Promise; - dirty(attr: String): boolean; - dirtyKeys(): string[]; - escape(attr: string): string; - existed(): boolean; - fetch(options?: Object.FetchOptions): Promise; - get(attr: string): any | undefined; - getACL(): ACL | undefined; - has(attr: string): boolean; - hasChanged(attr: string): boolean; - increment(attr: string, amount?: number): any; - isValid(): boolean; - op(attr: string): any; - previous(attr: string): any; - previousAttributes(): any; - relation(attr: string): Relation; - remove(attr: string, item: any): any; - save(attrs?: { [key: string]: any } | null, options?: Object.SaveOptions): Promise; - save(key: string, value: any, options?: Object.SaveOptions): Promise; - set(key: string, value: any, options?: Object.SetOptions): boolean; - setACL(acl: ACL, options?: SuccessFailureOptions): boolean; - unset(attr: string, options?: any): any; - validate(attrs: any, options?: SuccessFailureOptions): boolean; - } - - namespace Object { - interface DestroyOptions extends SuccessFailureOptions, WaitOption, ScopeOptions { } - - interface DestroyAllOptions extends SuccessFailureOptions, ScopeOptions { } - - interface FetchOptions extends SuccessFailureOptions, ScopeOptions { } - - interface SaveOptions extends SuccessFailureOptions, SilentOption, ScopeOptions, WaitOption { } - - interface SaveAllOptions extends SuccessFailureOptions, ScopeOptions { } - - interface SetOptions extends ErrorOption, SilentOption { - promise?: any; - } - } - - /** - * Every Parse application installed on a device registered for - * push notifications has an associated Installation object. - */ - class Installation extends Object { - - badge: any; - channels: string[]; - timeZone: any; - deviceType: string; - pushType: string; - installationId: string; - deviceToken: string; - channelUris: string; - appName: string; - appVersion: string; - parseVersion: string; - appIdentifier: string; - - } - - /** - * Creates a new instance with the given models and options. Typically, you - * will not call this method directly, but will instead make a subclass using - * Parse.Collection.extend. - * - * @param {Array} models An array of instances of Parse.Object. - * - * @param {Object} options An optional object with Backbone-style options. - * Valid options are:
    - *
  • model: The Parse.Object subclass that this collection contains. - *
  • query: An instance of Parse.Query to use when fetching items. - *
  • comparator: A string property name or function to sort by. - *
- * - * @see Parse.Collection.extend - * - * @class - * - *

Provides a standard collection class for our sets of models, ordered - * or unordered. For more information, see the - * Backbone - * documentation.

- */ - class Collection extends Events implements IBaseObject { - - model: Object; - models: Object[]; - query: Query; - comparator: (object: Object) => any; - - constructor(models?: Object[], options?: Collection.Options); - static extend(instanceProps: any, classProps: any): any; - - initialize(): void; - add(models: any[], options?: Collection.AddOptions): Collection; - at(index: number): Object; - //chain(): _._Chain>; - fetch(options?: Collection.FetchOptions): Promise; - create(model: Object, options?: Collection.CreateOptions): Object; - get(id: string): Object; - getByCid(cid: any): any; - pluck(attr: string): any[]; - remove(model: any, options?: Collection.RemoveOptions): Collection; - remove(models: any[], options?: Collection.RemoveOptions): Collection; - reset(models: any[], options?: Collection.ResetOptions): Collection; - sort(options?: Collection.SortOptions): Collection; - toJSON(): any; - } - - namespace Collection { - interface Options { - model?: Object; - query?: Query; - comparator?: string; - } - - interface AddOptions extends SilentOption { - /** - * The index at which to add the models. - */ - at?: number; - } - - interface CreateOptions extends SuccessFailureOptions, WaitOption, SilentOption, ScopeOptions { - } - - interface FetchOptions extends SuccessFailureOptions, SilentOption, ScopeOptions { } - - interface RemoveOptions extends SilentOption { } - - interface ResetOptions extends SilentOption { } - - interface SortOptions extends SilentOption { } - } - - /** - * @class - * - *

Parse.Events is a fork of Backbone's Events module, provided for your - * convenience.

- * - *

A module that can be mixed in to any object in order to provide - * it with custom events. You may bind callback functions to an event - * with `on`, or remove these functions with `off`. - * Triggering an event fires all callbacks in the order that `on` was - * called. - * - *

-     *     var object = {};
-     *     _.extend(object, Parse.Events);
-     *     object.on('expand', function(){ alert('expanded'); });
-     *     object.trigger('expand');

- * - *

For more information, see the - * Backbone - * documentation.

- */ - class Events { - - static off(events: string[], callback?: Function, context?: any): Events; - static on(events: string[], callback?: Function, context?: any): Events; - static trigger(events: string[]): Events; - static bind(): Events; - static unbind(): Events; - - on(eventName: string, callback?: Function, context?: any): Events; - off(eventName?: string | null, callback?: Function | null, context?: any): Events; - trigger(eventName: string, ...args: any[]): Events; - bind(eventName: string, callback: Function, context?: any): Events; - unbind(eventName?: string, callback?: Function, context?: any): Events; - - } - - /** - * Creates a new parse Parse.Query for the given Parse.Object subclass. - * @param objectClass - - * An instance of a subclass of Parse.Object, or a Parse className string. - * @class - * - *

Parse.Query defines a query that is used to fetch Parse.Objects. The - * most common use case is finding all objects that match a query through the - * find method. For example, this sample code fetches all objects - * of class MyClass. It calls a different function depending on - * whether the fetch succeeded or not. - * - *

-     * var query = new Parse.Query(MyClass);
-     * query.find({
-     *   success: function(results) {
-     *     // results is an array of Parse.Object.
-     *   },
-     *
-     *   error: function(error) {
-     *     // error is an instance of Parse.Error.
-     *   }
-     * });

- * - *

A Parse.Query can also be used to retrieve a single object whose id is - * known, through the get method. For example, this sample code fetches an - * object of class MyClass and id myId. It calls a - * different function depending on whether the fetch succeeded or not. - * - *

-     * var query = new Parse.Query(MyClass);
-     * query.get(myId, {
-     *   success: function(object) {
-     *     // object is an instance of Parse.Object.
-     *   },
-     *
-     *   error: function(object, error) {
-     *     // error is an instance of Parse.Error.
-     *   }
-     * });

- * - *

A Parse.Query can also be used to count the number of objects that match - * the query without retrieving all of those objects. For example, this - * sample code counts the number of objects of the class MyClass - *

-     * var query = new Parse.Query(MyClass);
-     * query.count({
-     *   success: function(number) {
-     *     // There are number instances of MyClass.
-     *   },
-     *
-     *   error: function(error) {
-     *     // error is an instance of Parse.Error.
-     *   }
-     * });

- */ - class Query extends BaseObject { - - objectClass: any; - className: string; - - constructor(objectClass: string); - constructor(objectClass: new (...args: any[]) => Object); - - static or(...var_args: Query[]): Query; - - addAscending(key: string): Query; - addAscending(key: string[]): Query; - addDescending(key: string): Query; - addDescending(key: string[]): Query; - ascending(key: string): Query; - ascending(key: string[]): Query; - collection(items?: Object[], options?: Collection.Options): Collection; - containedIn(key: string, values: any[]): Query; - contains(key: string, substring: string): Query; - containsAll(key: string, values: any[]): Query; - count(options?: Query.CountOptions): Promise; - descending(key: string): Query; - descending(key: string[]): Query; - doesNotExist(key: string): Query; - doesNotMatchKeyInQuery(key: string, queryKey: string, query: Query): Query; - doesNotMatchQuery(key: string, query: Query): Query; - each(callback: Function, options?: Query.EachOptions): Promise; - endsWith(key: string, suffix: string): Query; - equalTo(key: string, value: any): Query; - exists(key: string): Query; - find(options?: Query.FindOptions): Promise; - first(options?: Query.FirstOptions): Promise; - get(objectId: string, options?: Query.GetOptions): Promise; - greaterThan(key: string, value: any): Query; - greaterThanOrEqualTo(key: string, value: any): Query; - include(key: string): Query; - include(keys: string[]): Query; - lessThan(key: string, value: any): Query; - lessThanOrEqualTo(key: string, value: any): Query; - limit(n: number): Query; - matches(key: string, regex: RegExp, modifiers: any): Query; - matchesKeyInQuery(key: string, queryKey: string, query: Query): Query; - matchesQuery(key: string, query: Query): Query; - near(key: string, point: GeoPoint): Query; - notContainedIn(key: string, values: any[]): Query; - notEqualTo(key: string, value: any): Query; - select(...keys: string[]): Query; - skip(n: number): Query; - startsWith(key: string, prefix: string): Query; - withinGeoBox(key: string, southwest: GeoPoint, northeast: GeoPoint): Query; - withinKilometers(key: string, point: GeoPoint, maxDistance: number): Query; - withinMiles(key: string, point: GeoPoint, maxDistance: number): Query; - withinRadians(key: string, point: GeoPoint, maxDistance: number): Query; - } - - namespace Query { - interface EachOptions extends SuccessFailureOptions, ScopeOptions { } - interface CountOptions extends SuccessFailureOptions, ScopeOptions { } - interface FindOptions extends SuccessFailureOptions, ScopeOptions { } - interface FirstOptions extends SuccessFailureOptions, ScopeOptions { } - interface GetOptions extends SuccessFailureOptions, ScopeOptions { } - } - - /** - * Represents a Role on the Parse server. Roles represent groupings of - * Users for the purposes of granting permissions (e.g. specifying an ACL - * for an Object). Roles are specified by their sets of child users and - * child roles, all of which are granted any permissions that the parent - * role has. - * - *

Roles must have a name (which cannot be changed after creation of the - * role), and must specify an ACL.

- * @class - * A Parse.Role is a local representation of a role persisted to the Parse - * cloud. - */ - class Role extends Object { - - constructor(name: string, acl: ACL); - - getRoles(): Relation; - getUsers(): Relation; - getName(): string; - setName(name: string, options?: SuccessFailureOptions): any; - } - - class Config extends Object { - static get(options?: SuccessFailureOptions): Promise; - static current(): Config; - - get(attr: string): any; - escape(attr: string): any; - } - - class Session extends Object { - static current(): Promise; - - getSessionToken(): string; - isCurrentSessionRevocable(): boolean; - } - - /** - * Routers map faux-URLs to actions, and fire events when routes are - * matched. Creating a new one sets its `routes` hash, if not set statically. - * @class - * - *

A fork of Backbone.Router, provided for your convenience. - * For more information, see the - * Backbone - * documentation.

- *

Available in the client SDK only.

- */ - class Router extends Events { - - routes: Router.RouteMap; - - constructor(options?: Router.Options); - static extend(instanceProps: any, classProps: any): any; - - initialize(): void; - navigate(fragment: string, options?: Router.NavigateOptions): Router; - navigate(fragment: string, trigger?: boolean): Router; - route(route: string, name: string, callback: Function): Router; - } - - namespace Router { - interface Options { - routes: RouteMap; - } - - interface RouteMap { - [url: string]: string; - } - - interface NavigateOptions { - trigger?: boolean; - } - } - - /** - * @class - * - *

A Parse.User object is a local representation of a user persisted to the - * Parse cloud. This class is a subclass of a Parse.Object, and retains the - * same functionality of a Parse.Object, but also extends it with various - * user specific methods, like authentication, signing up, and validation of - * uniqueness.

- */ - class User extends Object { - - static current(): User | undefined; - static signUp(username: string, password: string, attrs: any, options?: SuccessFailureOptions): Promise; - static logIn(username: string, password: string, options?: SuccessFailureOptions): Promise; - static logOut(): Promise; - static allowCustomUserClass(isAllowed: boolean): void; - static become(sessionToken: string, options?: SuccessFailureOptions): Promise; - static requestPasswordReset(email: string, options?: SuccessFailureOptions): Promise; - static extend(protoProps?: any, classProps?: any): any; - - signUp(attrs: any, options?: SuccessFailureOptions): Promise; - logIn(options?: SuccessFailureOptions): Promise; - authenticated(): boolean; - isCurrent(): boolean; - - getEmail(): string | undefined; - setEmail(email: string, options?: SuccessFailureOptions): boolean; - - getUsername(): string | undefined; - setUsername(username: string, options?: SuccessFailureOptions): boolean; - - setPassword(password: string, options?: SuccessFailureOptions): boolean; - getSessionToken(): string; - } - - /** - * Creating a Parse.View creates its initial element outside of the DOM, - * if an existing element is not provided... - * @class - * - *

A fork of Backbone.View, provided for your convenience. If you use this - * class, you must also include jQuery, or another library that provides a - * jQuery-compatible $ function. For more information, see the - * Backbone - * documentation.

- *

Available in the client SDK only.

- */ - class View extends Events { - - model: any; - collection: any; - id: string; - cid: string; - className: string; - tagName: string; - el: any; - //$el: JQuery; - attributes: any; - - constructor(options?: View.Options); - - static extend(properties: any, classProperties?: any): any; - - //$(selector?: string): JQuery; - setElement(element: HTMLElement, delegate?: boolean): View; - //setElement(element: JQuery, delegate?: boolean): View; - render(): View; - remove(): View; - make(tagName: any, attributes?: View.Attribute[], content?: any): any; - delegateEvents(events?: any): any; - undelegateEvents(): any; - - } - - namespace View { - interface Options { - model?: any; - collection?: any; - el?: any; - id?: string; - className?: string; - tagName?: string; - attributes?: Attribute[]; - } - - interface Attribute { - [attributeName: string]: string | number | boolean; - } - } - - namespace Analytics { - - function track(name: string, dimensions: any): Promise; - } - - /** - * Provides a set of utilities for using Parse with Facebook. - * @namespace - * Provides a set of utilities for using Parse with Facebook. - */ - namespace FacebookUtils { - - function init(options?: any): void; - function isLinked(user: User): boolean; - function link(user: User, permissions: any, options?: SuccessFailureOptions): void; - function logIn(permissions: any, options?: SuccessFailureOptions): void; - function unlink(user: User, options?: SuccessFailureOptions): void; - } - - /** - * @namespace Contains functions for calling and declaring - * cloud functions. - *

- * Some functions are only available from Cloud Code. - *

- */ - namespace Cloud { - - interface CookieOptions { - domain?: string; - expires?: Date; - httpOnly?: boolean; - maxAge?: number; - path?: string; - secure?: boolean; - } - - interface HttpResponse { - //buffer?: Buffer; - cookies?: any; - data?: any; - headers?: any; - status?: number; - text?: string; - } - - interface JobRequest { - params: any; - } - - interface JobStatus { - error?: (response: any) => void; - message?: (response: any) => void; - success?: (response: any) => void; - } - - interface FunctionRequest { - installationId?: String; - master?: boolean; - params?: any; - user?: User; - } - - interface FunctionResponse { - success: (response: any) => void; - error: (response: any) => void; - } - - interface Cookie { - name?: string; - options?: CookieOptions; - value?: string; - } - - interface SaveRequest extends FunctionRequest { - object: Object; - } - - interface AfterSaveRequest extends SaveRequest { } - interface AfterDeleteRequest extends FunctionRequest { } - interface BeforeDeleteRequest extends FunctionRequest { } - interface BeforeDeleteResponse extends FunctionResponse { } - interface BeforeSaveRequest extends SaveRequest { } - interface BeforeSaveResponse extends FunctionResponse { - success: () => void; - } - - function afterDelete(arg1: any, func?: (request: AfterDeleteRequest) => void): void; - function afterSave(arg1: any, func?: (request: AfterSaveRequest) => void): void; - function beforeDelete(arg1: any, func?: (request: BeforeDeleteRequest, response: BeforeDeleteResponse) => void): void; - function beforeSave(arg1: any, func?: (request: BeforeSaveRequest, response: BeforeSaveResponse) => void): void; - function define(name: string, func?: (request: FunctionRequest, response: FunctionResponse) => void): void; - function httpRequest(options: HTTPOptions): Promise; - function job(name: string, func?: (request: JobRequest, status: JobStatus) => void): HttpResponse; - function run(name: string, data?: any, options?: RunOptions): Promise; - function useMasterKey(): void; - - interface RunOptions extends SuccessFailureOptions, ScopeOptions { } - - /** - * To use this Cloud Module in Cloud Code, you must require 'buffer' in your JavaScript file. - * - * import Buffer = require("buffer").Buffer; - */ - var HTTPOptions: new () => HTTPOptions; - interface HTTPOptions { - /** - * The body of the request. - * If it is a JSON object, then the Content-Type set in the headers must be application/x-www-form-urlencoded or application/json. - * You can also set this to a Buffer object to send raw bytes. - * If you use a Buffer, you should also set the Content-Type header explicitly to describe what these bytes represent. - */ - //body?: string | Buffer | Object; - body?: string | Object; - /** - * Defaults to 'false'. - */ - followRedirects?: boolean; - /** - * The headers for the request. - */ - headers?: { - [headerName: string]: string | number | boolean; - }; - /** - *The method of the request (i.e GET, POST, etc). - */ - method?: string; - /** - * The query portion of the url. - */ - params?: any; - /** - * The url to send the request to. - */ - url: string; - - success?: (response: any) => void; - error?: (response: any) => void; - } - } - - - class Error { - - code: ErrorCode; - message: string; - - constructor(code: ErrorCode, message: string); - - } - - /* - * We need to inline the codes in order to make compilation work without this type definition as dependency. - */ - const enum ErrorCode { - - OTHER_CAUSE = -1, - INTERNAL_SERVER_ERROR = 1, - CONNECTION_FAILED = 100, - OBJECT_NOT_FOUND = 101, - INVALID_QUERY = 102, - INVALID_CLASS_NAME = 103, - MISSING_OBJECT_ID = 104, - INVALID_KEY_NAME = 105, - INVALID_POINTER = 106, - INVALID_JSON = 107, - COMMAND_UNAVAILABLE = 108, - NOT_INITIALIZED = 109, - INCORRECT_TYPE = 111, - INVALID_CHANNEL_NAME = 112, - PUSH_MISCONFIGURED = 115, - OBJECT_TOO_LARGE = 116, - OPERATION_FORBIDDEN = 119, - CACHE_MISS = 120, - INVALID_NESTED_KEY = 121, - INVALID_FILE_NAME = 122, - INVALID_ACL = 123, - TIMEOUT = 124, - INVALID_EMAIL_ADDRESS = 125, - MISSING_CONTENT_TYPE = 126, - MISSING_CONTENT_LENGTH = 127, - INVALID_CONTENT_LENGTH = 128, - FILE_TOO_LARGE = 129, - FILE_SAVE_ERROR = 130, - DUPLICATE_VALUE = 137, - INVALID_ROLE_NAME = 139, - EXCEEDED_QUOTA = 140, - SCRIPT_FAILED = 141, - VALIDATION_ERROR = 142, - INVALID_IMAGE_DATA = 150, - UNSAVED_FILE_ERROR = 151, - INVALID_PUSH_TIME_ERROR = 152, - FILE_DELETE_ERROR = 153, - REQUEST_LIMIT_EXCEEDED = 155, - INVALID_EVENT_NAME = 160, - USERNAME_MISSING = 200, - PASSWORD_MISSING = 201, - USERNAME_TAKEN = 202, - EMAIL_TAKEN = 203, - EMAIL_MISSING = 204, - EMAIL_NOT_FOUND = 205, - SESSION_MISSING = 206, - MUST_CREATE_USER_THROUGH_SIGNUP = 207, - ACCOUNT_ALREADY_LINKED = 208, - INVALID_SESSION_TOKEN = 209, - LINKED_ID_MISSING = 250, - INVALID_LINKED_SESSION = 251, - UNSUPPORTED_SERVICE = 252, - AGGREGATE_ERROR = 600, - FILE_READ_ERROR = 601, - X_DOMAIN_REQUEST = 602 - } - - /** - * @class - * A Parse.Op is an atomic operation that can be applied to a field in a - * Parse.Object. For example, calling object.set("foo", "bar") - * is an example of a Parse.Op.Set. Calling object.unset("foo") - * is a Parse.Op.Unset. These operations are stored in a Parse.Object and - * sent to the server as part of object.save() operations. - * Instances of Parse.Op should be immutable. - * - * You should not create subclasses of Parse.Op or instantiate Parse.Op - * directly. - */ - namespace Op { - - interface BaseOperation extends IBaseObject { - objects(): any[]; - } - - interface Add extends BaseOperation { - } - - interface AddUnique extends BaseOperation { - } - - interface Increment extends IBaseObject { - amount: number; - } - - interface Relation extends IBaseObject { - added(): Object[]; - removed: Object[]; - } - - interface Set extends IBaseObject { - value(): any; - } - - interface Unset extends IBaseObject { - } - - } - - /** - * Contains functions to deal with Push in Parse - * @name Parse.Push - * @namespace - */ - namespace Push { - function send(data: PushData, options?: SendOptions): Promise; - - interface PushData { - channels?: string[]; - push_time?: Date; - expiration_time?: Date; - expiration_interval?: number; - where?: Query; - data?: any; - alert?: string; - badge?: string; - sound?: string; - title?: string; - } - - interface SendOptions extends UseMasterKeyOption { - success?: () => void; - error?: (error: Error) => void; - } - } - - /** - * Call this method first to set up your authentication tokens for Parse. - * You can get your keys from the Data Browser on parse.com. - * @param {String} applicationId Your Parse Application ID. - * @param {String} javaScriptKey (optional) Your Parse JavaScript Key (Not needed for parse-server) - * @param {String} masterKey (optional) Your Parse Master Key. (Node.js only!) - */ - function initialize(applicationId: string, javaScriptKey?: string, masterKey?: string): void; - -} - -declare module "parse/node" { - export = Parse; -} - -declare module "parse" { - import * as parse from "parse/node"; - export = parse -} \ No newline at end of file diff --git a/src/multi/Connection.spec.ts b/src/multi/Connection.spec.ts index a86f9e9..f1f2abc 100644 --- a/src/multi/Connection.spec.ts +++ b/src/multi/Connection.spec.ts @@ -1,86 +1,93 @@ -module TK.SpaceTac.Multi.Specs { - testing("Connection", test => { - test.acase("finds an unused token", async check => { - let storage = new FakeRemoteStorage(); - let connection = new Connection("test", storage); +import { NAMESPACE } from ".."; +import { Serializer } from "../common/Serializer"; +import { testing } from "../common/Testing"; +import { items, iterator, nn, nnf } from "../common/Tools"; +import { GameSession } from "../core/GameSession"; +import { Player } from "../core/Player"; +import { Connection } from "./Connection"; +import { FakeRemoteStorage } from "./RemoteStorage"; - let token = await connection.getUnusedToken(5); - check.equals(token.length, 5); +testing("Connection", test => { + test.acase("finds an unused token", async check => { + let storage = new FakeRemoteStorage(); + let connection = new Connection("test", storage); - await storage.upsert("sessioninfo", { token: token }, {}); + let token = await connection.getUnusedToken(5); + check.equals(token.length, 5); - check.patch(connection, "generateToken", nnf("", iterator([token, "123456"]))); + await storage.upsert("sessioninfo", { token: token }, {}); - let other = await connection.getUnusedToken(5); - check.equals(other, "123456"); - }); + check.patch(connection, "generateToken", nnf("", iterator([token, "123456"]))); - test.acase("loads a session by its id", async check => { - let session = new GameSession(); - let serializer = new Serializer(TK.SpaceTac); - let storage = new FakeRemoteStorage(); - let connection = new Connection("test", storage); + let other = await connection.getUnusedToken(5); + check.equals(other, "123456"); + }); - let result = await connection.loadById("abc"); - check.equals(result, null); + test.acase("loads a session by its id", async check => { + let session = new GameSession(); + let serializer = new Serializer(NAMESPACE); + let storage = new FakeRemoteStorage(); + let connection = new Connection("test", storage); - await storage.upsert("session", { ref: "abc" }, { data: serializer.serialize(session) }); + let result = await connection.loadById("abc"); + check.equals(result, null); - result = await connection.loadById("abc"); - check.equals(result, session); - result = await connection.loadById("abcd"); - check.equals(result, null); + await storage.upsert("session", { ref: "abc" }, { data: serializer.serialize(session) }); - // even from another device - let other = new Connection("notest", storage); - result = await other.loadById("abc"); - check.equals(result, session); + result = await connection.loadById("abc"); + check.equals(result, session); + result = await connection.loadById("abcd"); + check.equals(result, null); - // do not load if it is not a GameSession - await storage.upsert("session", { ref: "abcd" }, { data: serializer.serialize(new Player()) }); - result = await connection.loadById("abcd"); - check.equals(result, null); - }); + // even from another device + let other = new Connection("notest", storage); + result = await other.loadById("abc"); + check.equals(result, session); - test.acase("lists saves from a device", async check => { - let storage = new FakeRemoteStorage(); - let connection = new Connection("test", storage); + // do not load if it is not a GameSession + await storage.upsert("session", { ref: "abcd" }, { data: serializer.serialize(new Player()) }); + result = await connection.loadById("abcd"); + check.equals(result, null); + }); - let result = await connection.listSaves(); - check.equals(result, {}); + test.acase("lists saves from a device", async check => { + let storage = new FakeRemoteStorage(); + let connection = new Connection("test", storage); - await storage.upsert("sessioninfo", { device: "test", ref: "abc" }, { info: "ABC" }); - await storage.upsert("sessioninfo", { device: "other", ref: "abcd" }, { info: "ABCD" }); - await storage.upsert("sessioninfo", { device: "test", ref: "cba" }, { info: "CBA" }); + let result = await connection.listSaves(); + check.equals(result, {}); - result = await connection.listSaves(); - check.equals(result, { abc: "ABC", cba: "CBA" }); - }); + await storage.upsert("sessioninfo", { device: "test", ref: "abc" }, { info: "ABC" }); + await storage.upsert("sessioninfo", { device: "other", ref: "abcd" }, { info: "ABCD" }); + await storage.upsert("sessioninfo", { device: "test", ref: "cba" }, { info: "CBA" }); - test.acase("publishes saves and retrieves them by token", async check => { - let session = new GameSession(); - let storage = new FakeRemoteStorage(); - let connection = new Connection("test", storage); + result = await connection.listSaves(); + check.equals(result, { abc: "ABC", cba: "CBA" }); + }); - let saves = await connection.listSaves(); - check.equals(items(saves).length, 0); + test.acase("publishes saves and retrieves them by token", async check => { + let session = new GameSession(); + let storage = new FakeRemoteStorage(); + let connection = new Connection("test", storage); - let token = await connection.publish(session, "TEST"); + let saves = await connection.listSaves(); + check.equals(items(saves).length, 0); - saves = await connection.listSaves(); - check.equals(items(saves).length, 1); + let token = await connection.publish(session, "TEST"); - let loaded = await connection.loadByToken(token); - check.equals(loaded, session); + saves = await connection.listSaves(); + check.equals(items(saves).length, 1); - let newtoken = await connection.publish(nn(loaded), "TEST"); - check.equals(token, newtoken); + let loaded = await connection.loadByToken(token); + check.equals(loaded, session); - loaded = await connection.loadByToken(token); - check.equals(loaded, session); + let newtoken = await connection.publish(nn(loaded), "TEST"); + check.equals(token, newtoken); - saves = await connection.listSaves(); - check.equals(items(saves).length, 1); - }); - }); -} + loaded = await connection.loadByToken(token); + check.equals(loaded, session); + + saves = await connection.listSaves(); + check.equals(items(saves).length, 1); + }); +}); diff --git a/src/multi/Connection.ts b/src/multi/Connection.ts index 9ff23b1..9813f61 100644 --- a/src/multi/Connection.ts +++ b/src/multi/Connection.ts @@ -1,89 +1,94 @@ -module TK.SpaceTac.Multi { - /** - * Multiplayer connection to a Parse server - */ - export class Connection { - device_id: string - serializer = new Serializer(TK.SpaceTac) - token_chars = "abcdefghjkmnpqrstuvwxyz123456789" - storage: IRemoteStorage +import { NAMESPACE } from ".."; +import { RandomGenerator } from "../common/RandomGenerator"; +import { Serializer } from "../common/Serializer"; +import { dict, range } from "../common/Tools"; +import { GameSession } from "../core/GameSession"; +import { IRemoteStorage } from "./RemoteStorage"; - constructor(device_id: string, storage: IRemoteStorage) { - this.device_id = device_id; - this.storage = storage; - } +/** + * Multiplayer connection to a Parse server + */ +export class Connection { + device_id: string + serializer = new Serializer(NAMESPACE) + token_chars = "abcdefghjkmnpqrstuvwxyz123456789" + storage: IRemoteStorage - /** - * Generate a random token - */ - generateToken(length: number): string { - return range(length).map(() => RandomGenerator.global.choice(this.token_chars)).join(""); - } + constructor(device_id: string, storage: IRemoteStorage) { + this.device_id = device_id; + this.storage = storage; + } - /** - * Find an unused session token - */ - async getUnusedToken(length = 5): Promise { - let token = this.generateToken(length); - let existing = await this.storage.search("sessioninfo", { token: token }); - if (existing.length > 0) { - token = await this.getUnusedToken(length + 1); - } - return token; - } + /** + * Generate a random token + */ + generateToken(length: number): string { + return range(length).map(() => RandomGenerator.global.choice(this.token_chars)).join(""); + } - /** - * Publish a session to remote server, and return an invitation token - */ - async publish(session: GameSession, description: string): Promise { - await this.storage.upsert("session", { ref: session.id }, { data: this.serializer.serialize(session) }); - - let now = new Date(); - let date = now.toISOString().substr(0, 10) + " " + now.toTimeString().substr(0, 5); - let info = `${date}\n${description}`; - - let sessinfo = await this.storage.find("sessioninfo", { ref: session.id, device: this.device_id }); - let token: string = sessinfo ? sessinfo.token : ""; - if (token.length == 0) { - token = await this.getUnusedToken(); - } - await this.storage.upsert("sessioninfo", { ref: session.id, device: this.device_id, token: token }, { info: info }); - - return token; - } - - /** - * Load a session from a remote server, by its token - */ - async loadByToken(token: string): Promise { - let info = await this.storage.find("sessioninfo", { token: token }); - if (info) { - return this.loadById(info.ref); - } else { - return null; - } - } - - /** - * Load a session from a remote server, by its id - */ - async loadById(id: string): Promise { - let session = await this.storage.find("session", { ref: id }); - if (session) { - let loaded = this.serializer.unserialize(session.data); - if (loaded instanceof GameSession) { - return loaded; - } - } - return null; - } - - /** - * List cloud saves, associated with current device - */ - async listSaves(): Promise<{ [id: string]: string }> { - let results = await this.storage.search("sessioninfo", { device: this.device_id }); - return dict(results.map(obj => <[string, string]>[obj.ref, obj.info])); - } + /** + * Find an unused session token + */ + async getUnusedToken(length = 5): Promise { + let token = this.generateToken(length); + let existing = await this.storage.search("sessioninfo", { token: token }); + if (existing.length > 0) { + token = await this.getUnusedToken(length + 1); } + return token; + } + + /** + * Publish a session to remote server, and return an invitation token + */ + async publish(session: GameSession, description: string): Promise { + await this.storage.upsert("session", { ref: session.id }, { data: this.serializer.serialize(session) }); + + let now = new Date(); + let date = now.toISOString().substr(0, 10) + " " + now.toTimeString().substr(0, 5); + let info = `${date}\n${description}`; + + let sessinfo = await this.storage.find("sessioninfo", { ref: session.id, device: this.device_id }); + let token: string = sessinfo ? sessinfo.token : ""; + if (token.length == 0) { + token = await this.getUnusedToken(); + } + await this.storage.upsert("sessioninfo", { ref: session.id, device: this.device_id, token: token }, { info: info }); + + return token; + } + + /** + * Load a session from a remote server, by its token + */ + async loadByToken(token: string): Promise { + let info = await this.storage.find("sessioninfo", { token: token }); + if (info) { + return this.loadById(info.ref); + } else { + return null; + } + } + + /** + * Load a session from a remote server, by its id + */ + async loadById(id: string): Promise { + let session = await this.storage.find("session", { ref: id }); + if (session) { + let loaded = this.serializer.unserialize(session.data); + if (loaded instanceof GameSession) { + return loaded; + } + } + return null; + } + + /** + * List cloud saves, associated with current device + */ + async listSaves(): Promise<{ [id: string]: string }> { + let results = await this.storage.search("sessioninfo", { device: this.device_id }); + return dict(results.map(obj => <[string, string]>[obj.ref, obj.info])); + } } diff --git a/src/multi/Exchange.spec.ts b/src/multi/Exchange.spec.ts index c6a82b1..12218a8 100644 --- a/src/multi/Exchange.spec.ts +++ b/src/multi/Exchange.spec.ts @@ -1,64 +1,69 @@ -module TK.SpaceTac.Multi.Specs { - testing("Exchange", test => { - function newExchange(token: string, storage = new FakeRemoteStorage()): [FakeRemoteStorage, Exchange, Exchange] { - let connection = new Connection("test", storage); +import { testing } from "../common/Testing"; +import { Timer } from "../common/Timer"; +import { iterator, nnf } from "../common/Tools"; +import { Connection } from "./Connection"; +import { Exchange } from "./Exchange"; +import { FakeRemoteStorage } from "./RemoteStorage"; - let peer1 = new Exchange(connection, token, true, "peer1"); - let peer2 = new Exchange(connection, token, false, "peer2"); - peer1.timer = new Timer(true); - peer2.timer = new Timer(true); - /*peer1.debug = true; - peer2.debug = true;*/ +testing("Exchange", test => { + function newExchange(token: string, storage = new FakeRemoteStorage()): [FakeRemoteStorage, Exchange, Exchange] { + let connection = new Connection("test", storage); - return [storage, peer1, peer2]; - } + let peer1 = new Exchange(connection, token, true, "peer1"); + let peer2 = new Exchange(connection, token, false, "peer2"); + peer1.timer = new Timer(true); + peer2.timer = new Timer(true); + /*peer1.debug = true; + peer2.debug = true;*/ - test.setup(function () { - test.check.patch(console, "log", null); - }); + return [storage, peer1, peer2]; + } - test.acase("says hello on start", async check => { - let [storage, peer1, peer2] = newExchange("abc"); - check.patch(peer1, "getNextId", nnf("", iterator(["1A", "1B", "1C"]))); - check.patch(peer2, "getNextId", nnf("", iterator(["2A", "2B", "2C"]))); + test.setup(function () { + test.check.patch(console, "log", null); + }); - check.equals(peer1.next, "hello"); - check.equals(peer2.next, "hello"); - check.equals(peer1.remotepeer, null); - check.equals(peer2.remotepeer, null); - check.equals(peer1.writing, true); - check.equals(peer2.writing, false); - check.equals(peer1.count, 0); - check.equals(peer2.count, 0); + test.acase("says hello on start", async check => { + let [storage, peer1, peer2] = newExchange("abc"); + check.patch(peer1, "getNextId", nnf("", iterator(["1A", "1B", "1C"]))); + check.patch(peer2, "getNextId", nnf("", iterator(["2A", "2B", "2C"]))); - await Promise.all([peer1.start(), peer2.start()]); + check.equals(peer1.next, "hello"); + check.equals(peer2.next, "hello"); + check.equals(peer1.remotepeer, null); + check.equals(peer2.remotepeer, null); + check.equals(peer1.writing, true); + check.equals(peer2.writing, false); + check.equals(peer1.count, 0); + check.equals(peer2.count, 0); - check.equals(storage.collections["exchange"], [ - { peer: "peer1", current: "hello", next: "1A", count: 0, token: "abc", over: true, data: null }, - { peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null }, - ]); + await Promise.all([peer1.start(), peer2.start()]); - check.equals(peer1.next, "2A"); - check.equals(peer2.next, "2A"); - check.equals(peer1.remotepeer, "peer2"); - check.equals(peer2.remotepeer, "peer1"); - check.equals(peer1.writing, true); - check.equals(peer2.writing, false); - check.equals(peer1.count, 2); - check.equals(peer2.count, 2); + check.equals(storage.collections["exchange"], [ + { peer: "peer1", current: "hello", next: "1A", count: 0, token: "abc", over: true, data: null }, + { peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null }, + ]); - // same peers, new message chain - [storage, peer1, peer2] = newExchange("abc", storage); - check.patch(peer1, "getNextId", nnf("", iterator(["1R", "1S", "1T"]))); - check.patch(peer2, "getNextId", nnf("", iterator(["2R", "2S", "2T"]))); + check.equals(peer1.next, "2A"); + check.equals(peer2.next, "2A"); + check.equals(peer1.remotepeer, "peer2"); + check.equals(peer2.remotepeer, "peer1"); + check.equals(peer1.writing, true); + check.equals(peer2.writing, false); + check.equals(peer1.count, 2); + check.equals(peer2.count, 2); - await Promise.all([peer1.start(), peer2.start()]); + // same peers, new message chain + [storage, peer1, peer2] = newExchange("abc", storage); + check.patch(peer1, "getNextId", nnf("", iterator(["1R", "1S", "1T"]))); + check.patch(peer2, "getNextId", nnf("", iterator(["2R", "2S", "2T"]))); - check.equals(storage.collections["exchange"], [ - { peer: "peer1", current: "hello", next: "1R", count: 0, token: "abc", over: true, data: null }, - { peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null }, - { peer: "peer2", current: "1R", next: "2R", count: 1, token: "abc", over: true, data: null }, - ]); - }) - }) -} \ No newline at end of file + await Promise.all([peer1.start(), peer2.start()]); + + check.equals(storage.collections["exchange"], [ + { peer: "peer1", current: "hello", next: "1R", count: 0, token: "abc", over: true, data: null }, + { peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null }, + { peer: "peer2", current: "1R", next: "2R", count: 1, token: "abc", over: true, data: null }, + ]); + }) +}) diff --git a/src/multi/Exchange.ts b/src/multi/Exchange.ts index 14f6a98..467cf31 100644 --- a/src/multi/Exchange.ts +++ b/src/multi/Exchange.ts @@ -1,121 +1,123 @@ -module TK.SpaceTac.Multi { - /** - * Network communication of two peers around a shared session - * - * An exchange is a sequence of messages - */ - export class Exchange { - connection: Connection - token: string - writing: boolean - localpeer: string - count = 0 - remotepeer: string | null = null - next = "hello" - closed = false - timer = new Timer() - debug = false +import { RandomGenerator } from "../common/RandomGenerator" +import { Timer } from "../common/Timer" +import { Connection } from "./Connection" - constructor(connection: Connection, token: string, initiator = false, myid = connection.device_id) { - this.connection = connection; - this.token = token; - this.localpeer = myid; - this.writing = initiator; - } +/** + * Network communication of two peers around a shared session + * + * An exchange is a sequence of messages + */ +export class Exchange { + connection: Connection + token: string + writing: boolean + localpeer: string + count = 0 + remotepeer: string | null = null + next = "hello" + closed = false + timer = new Timer() + debug = false - /** - * Initialize communication with the other peer - */ - async start(): Promise { - if (this.debug) { - console.debug("Exchange started", this.localpeer); - } + constructor(connection: Connection, token: string, initiator = false, myid = connection.device_id) { + this.connection = connection; + this.token = token; + this.localpeer = myid; + this.writing = initiator; + } - if (this.writing) { - await this.writeMessage(null, true); - await this.readMessage(); - } else { - await this.readMessage(); - await this.writeMessage(null, true); - } - - console.log("Exchange established", this.token, this.localpeer, this.remotepeer); - } - - /** - * Get a next frame id - */ - getNextId(): string { - return `${this.token}${this.count}${RandomGenerator.global.id(8)}`; - } - - /** - * Push a raw message - */ - async writeMessage(message: any, over: boolean) { - if (this.writing) { - if (this.debug) { - console.debug("Exchange write", this.localpeer, this.token, this.next); - } - - let futurenext = this.getNextId(); - let result = await this.connection.storage.upsert("exchange", { token: this.token, current: this.next }, - { peer: this.localpeer, over: over, data: message, next: futurenext, count: this.count }); - - this.count += 1; - this.next = futurenext; - if (over) { - this.writing = false; - } - return result; - } else { - throw new Error("[Exchange] Tried to write out-of-turn"); - } - } - - /** - * Wait for a single message - */ - async readMessage(): Promise { - if (this.writing) { - throw new Error("[Exchange] Tried to read out-of-turn"); - } else { - let message: any; - do { - if (this.debug) { - console.debug("Exchange read", this.localpeer, this.token, this.next); - } - message = await this.connection.storage.find("exchange", { token: this.token, current: this.next }); - if (!message) { - await this.timer.sleep(1000); - } - } while (!message); - - if (this.remotepeer) { - if (message.peer != this.remotepeer) { - throw new Error("[Exchange] Bad peer id"); - } - } else { - if (message.peer) { - this.remotepeer = message.peer; - } else { - throw new Error("[Exchange] No peer id"); - } - } - - if (message.count != this.count) { - throw new Error("[Exchange] Bad message count"); - } else { - this.count += 1; - } - - this.next = message.next; - if (message.over) { - this.writing = true; - } - - return message.data; - } - } + /** + * Initialize communication with the other peer + */ + async start(): Promise { + if (this.debug) { + console.debug("Exchange started", this.localpeer); } -} \ No newline at end of file + + if (this.writing) { + await this.writeMessage(null, true); + await this.readMessage(); + } else { + await this.readMessage(); + await this.writeMessage(null, true); + } + + console.log("Exchange established", this.token, this.localpeer, this.remotepeer); + } + + /** + * Get a next frame id + */ + getNextId(): string { + return `${this.token}${this.count}${RandomGenerator.global.id(8)}`; + } + + /** + * Push a raw message + */ + async writeMessage(message: any, over: boolean) { + if (this.writing) { + if (this.debug) { + console.debug("Exchange write", this.localpeer, this.token, this.next); + } + + let futurenext = this.getNextId(); + let result = await this.connection.storage.upsert("exchange", { token: this.token, current: this.next }, + { peer: this.localpeer, over: over, data: message, next: futurenext, count: this.count }); + + this.count += 1; + this.next = futurenext; + if (over) { + this.writing = false; + } + return result; + } else { + throw new Error("[Exchange] Tried to write out-of-turn"); + } + } + + /** + * Wait for a single message + */ + async readMessage(): Promise { + if (this.writing) { + throw new Error("[Exchange] Tried to read out-of-turn"); + } else { + let message: any; + do { + if (this.debug) { + console.debug("Exchange read", this.localpeer, this.token, this.next); + } + message = await this.connection.storage.find("exchange", { token: this.token, current: this.next }); + if (!message) { + await this.timer.sleep(1000); + } + } while (!message); + + if (this.remotepeer) { + if (message.peer != this.remotepeer) { + throw new Error("[Exchange] Bad peer id"); + } + } else { + if (message.peer) { + this.remotepeer = message.peer; + } else { + throw new Error("[Exchange] No peer id"); + } + } + + if (message.count != this.count) { + throw new Error("[Exchange] Bad message count"); + } else { + this.count += 1; + } + + this.next = message.next; + if (message.over) { + this.writing = true; + } + + return message.data; + } + } +} diff --git a/src/multi/RemoteStorage.spec.ts b/src/multi/RemoteStorage.spec.ts index 0c75ce3..39568da 100644 --- a/src/multi/RemoteStorage.spec.ts +++ b/src/multi/RemoteStorage.spec.ts @@ -1,51 +1,53 @@ -module TK.SpaceTac.Multi.Specs { - testing("FakeRemoteStorage", test => { - test.acase("can fetch a single record", async check => { - let storage = new FakeRemoteStorage(); +import { testing } from "../common/Testing"; +import { sortedBy } from "../common/Tools"; +import { FakeRemoteStorage } from "./RemoteStorage"; - let result = await storage.find("test", { key: 5 }); - check.equals(result, null); +testing("FakeRemoteStorage", test => { + test.acase("can fetch a single record", async check => { + let storage = new FakeRemoteStorage(); - await storage.upsert("test", { key: 5 }, { text: "thingy" }); + let result = await storage.find("test", { key: 5 }); + check.equals(result, null); - result = await storage.find("test", { key: 5 }); - check.equals(result, { key: 5, text: "thingy" }); + await storage.upsert("test", { key: 5 }, { text: "thingy" }); - result = await storage.find("test", { key: 6 }); - check.equals(result, null); + result = await storage.find("test", { key: 5 }); + check.equals(result, { key: 5, text: "thingy" }); - result = await storage.find("test", { key: 5, text: "thingy" }); - check.equals(result, { key: 5, text: "thingy" }); + result = await storage.find("test", { key: 6 }); + check.equals(result, null); - result = await storage.find("notest", { key: 5 }); - check.equals(result, null); - }); + result = await storage.find("test", { key: 5, text: "thingy" }); + check.equals(result, { key: 5, text: "thingy" }); - test.acase("inserts or updates objects", async check => { - let storage = new FakeRemoteStorage(); + result = await storage.find("notest", { key: 5 }); + check.equals(result, null); + }); - let result = await storage.search("test", { key: 5 }); - check.equals(result, []); + test.acase("inserts or updates objects", async check => { + let storage = new FakeRemoteStorage(); - await storage.upsert("test", { key: 5 }, {}); + let result = await storage.search("test", { key: 5 }); + check.equals(result, []); - result = await storage.search("test", { key: 5 }); - check.equals(result, [{ key: 5 }]); + await storage.upsert("test", { key: 5 }, {}); - await storage.upsert("test", { key: 5 }, { text: "thingy" }); + result = await storage.search("test", { key: 5 }); + check.equals(result, [{ key: 5 }]); - result = await storage.search("test", { key: 5 }); - check.equals(result, [{ key: 5, text: "thingy" }]); + await storage.upsert("test", { key: 5 }, { text: "thingy" }); - await storage.upsert("test", { key: 5 }, { text: "other thingy" }); + result = await storage.search("test", { key: 5 }); + check.equals(result, [{ key: 5, text: "thingy" }]); - result = await storage.search("test", { key: 5 }); - check.equals(result, [{ key: 5, text: "other thingy" }]); + await storage.upsert("test", { key: 5 }, { text: "other thingy" }); - await storage.upsert("test", { key: 5, text: "things" }, {}); + result = await storage.search("test", { key: 5 }); + check.equals(result, [{ key: 5, text: "other thingy" }]); - result = await storage.search("test", { key: 5 }); - check.equals(sortedBy(result, (x: any) => x.text), [{ key: 5, text: "other thingy" }, { key: 5, text: "things" }]); - }); - }); -} \ No newline at end of file + await storage.upsert("test", { key: 5, text: "things" }, {}); + + result = await storage.search("test", { key: 5 }); + check.equals(sortedBy(result, (x: any) => x.text), [{ key: 5, text: "other thingy" }, { key: 5, text: "things" }]); + }); +}); diff --git a/src/multi/RemoteStorage.ts b/src/multi/RemoteStorage.ts index b275ade..e09232d 100644 --- a/src/multi/RemoteStorage.ts +++ b/src/multi/RemoteStorage.ts @@ -1,125 +1,123 @@ -/// +import { any, copy, copyfields, items, iteritems } from "../common/Tools"; -module TK.SpaceTac.Multi { - /** - * Interface for a remote storage, used for networking/multiplayer features - */ - export interface IRemoteStorage { - /** - * Search through a collection for equality of some fields - */ - search(collection: string, fields: any): Promise - /** - * Find a single object with equality of some fields - */ - find(collection: string, fields: any): Promise - /** - * Insert or update an object in a collection, based on some unicity fields - */ - upsert(collection: string, unicity: any, additional: any): Promise - } - - /** - * Remote storage using the Parse protocol - */ - export class ParseRemoteStorage implements IRemoteStorage { - constructor() { - Parse.initialize("thunderk.net"); - Parse.serverURL = 'https://rs.thunderk.net/parse'; - } - - /** - * Unpack a Parse.Object to a javascript object - */ - static unpack(obj: Parse.Object): Object { - return obj.toJSON(); - } - - /** - * Get the Parse model for a given collection name. - */ - private getModel(collection: string): any { - return Parse.Object.extend("spacetac" + collection); - } - - async search(collection: string, fields: any): Promise { - let query = new Parse.Query(this.getModel(collection)); - iteritems(fields, (key, value) => { - query.equalTo(key, value); - }); - - let results = await query.find(); - return results.map(ParseRemoteStorage.unpack); - } - - async find(collection: string, fields: any): Promise { - let results = await this.search(collection, fields); - if (results.length == 1) { - return results[0]; - } else { - return null; - } - } - - async upsert(collection: string, unicity: any, additional: any): Promise { - let query = new Parse.Query(this.getModel(collection)); - iteritems(unicity, (key, value) => { - query.equalTo(key, value); - }); - - let results = await query.find(); - let model = this.getModel(collection); - let base = new model(); - if (results.length == 1) { - base = results[0]; - } else { - iteritems(unicity, (key, value) => { - base.set(key, value); - }); - } - - iteritems(additional, (key, value) => { - base.set(key, value); - }); - await base.save(); - } - } - - /** - * Fake remote storage in memory (for testing purposes) - */ - export class FakeRemoteStorage implements IRemoteStorage { - collections: { [collection: string]: any[] } = {} - getCollection(name: string): any { - let collection = this.collections[name]; - if (collection) { - return collection; - } else { - this.collections[name] = []; - return this.collections[name]; - } - } - async search(collection: string, fields: any): Promise { - let objects = this.getCollection(collection); - let result = objects.filter((obj: any) => !any(items(fields), ([key, value]) => obj[key] != value)); - return result; - } - async find(collection: string, fields: any): Promise { - let results = await this.search(collection, fields); - if (results.length == 1) { - return results[0]; - } else { - return null; - } - } - async upsert(collection: string, unicity: any, additional: any): Promise { - let existing = await this.find(collection, unicity); - let base = existing || copy(unicity); - copyfields(additional, base); - if (!existing) { - let objects = this.getCollection(collection); - objects.push(base); - } - } - } +/** + * Interface for a remote storage, used for networking/multiplayer features + */ +export interface IRemoteStorage { + /** + * Search through a collection for equality of some fields + */ + search(collection: string, fields: any): Promise + /** + * Find a single object with equality of some fields + */ + find(collection: string, fields: any): Promise + /** + * Insert or update an object in a collection, based on some unicity fields + */ + upsert(collection: string, unicity: any, additional: any): Promise +} + +/** + * Remote storage using the Parse protocol + */ +export class ParseRemoteStorage implements IRemoteStorage { + constructor() { + Parse.initialize("thunderk.net"); + Parse.serverURL = 'https://rs.thunderk.net/parse'; + } + + /** + * Unpack a Parse.Object to a javascript object + */ + static unpack(obj: Parse.Object): Object { + return obj.toJSON(); + } + + /** + * Get the Parse model for a given collection name. + */ + private getModel(collection: string): any { + return Parse.Object.extend("spacetac" + collection); + } + + async search(collection: string, fields: any): Promise { + let query = new Parse.Query(this.getModel(collection)); + iteritems(fields, (key, value) => { + query.equalTo(key, value); + }); + + let results = await query.find(); + return results.map(ParseRemoteStorage.unpack); + } + + async find(collection: string, fields: any): Promise { + let results = await this.search(collection, fields); + if (results.length == 1) { + return results[0]; + } else { + return null; + } + } + + async upsert(collection: string, unicity: any, additional: any): Promise { + let query = new Parse.Query(this.getModel(collection)); + iteritems(unicity, (key, value) => { + query.equalTo(key, value); + }); + + let results = await query.find(); + let model = this.getModel(collection); + let base = new model(); + if (results.length == 1) { + base = results[0]; + } else { + iteritems(unicity, (key, value) => { + base.set(key, value); + }); + } + + iteritems(additional, (key, value) => { + base.set(key, value); + }); + await base.save(); + } +} + +/** + * Fake remote storage in memory (for testing purposes) + */ +export class FakeRemoteStorage implements IRemoteStorage { + collections: { [collection: string]: any[] } = {} + getCollection(name: string): any { + let collection = this.collections[name]; + if (collection) { + return collection; + } else { + this.collections[name] = []; + return this.collections[name]; + } + } + async search(collection: string, fields: any): Promise { + let objects = this.getCollection(collection); + let result = objects.filter((obj: any) => !any(items(fields), ([key, value]) => obj[key] != value)); + return result; + } + async find(collection: string, fields: any): Promise { + let results = await this.search(collection, fields); + if (results.length == 1) { + return results[0]; + } else { + return null; + } + } + async upsert(collection: string, unicity: any, additional: any): Promise { + let existing = await this.find(collection, unicity); + let base = existing || copy(unicity); + copyfields(additional, base); + if (!existing) { + let objects = this.getCollection(collection); + objects.push(base); + } + } } diff --git a/src/ui/AssetLoading.spec.ts b/src/ui/AssetLoading.spec.ts index 8d27948..2911fa4 100644 --- a/src/ui/AssetLoading.spec.ts +++ b/src/ui/AssetLoading.spec.ts @@ -1,11 +1,13 @@ -module TK.SpaceTac.UI.Specs { - testing("AssetLoading", test => { - let testgame = setupSingleView(test, () => [new AssetLoading({}), []]); +import { testing } from "../common/Testing"; +import { AssetLoading } from "./AssetLoading"; +import { setupSingleView } from "./TestGame"; - test.case("builds cache keys from path", check => { - check.equals(AssetLoading.getKey("dir/file-path"), "dir-file-path"); - check.equals(AssetLoading.getKey("dir/file-path.ext"), "dir-file-path"); - check.equals(AssetLoading.getKey("dir/file-path.mp3"), "dir-file-path"); - }); - }); -} +testing("AssetLoading", test => { + let testgame = setupSingleView(test, () => [new AssetLoading({}), []]); + + test.case("builds cache keys from path", check => { + check.equals(AssetLoading.getKey("dir/file-path"), "dir-file-path"); + check.equals(AssetLoading.getKey("dir/file-path.ext"), "dir-file-path"); + check.equals(AssetLoading.getKey("dir/file-path.mp3"), "dir-file-path"); + }); +}); diff --git a/src/ui/AssetLoading.ts b/src/ui/AssetLoading.ts index dd4509d..7c2a8b9 100644 --- a/src/ui/AssetLoading.ts +++ b/src/ui/AssetLoading.ts @@ -1,86 +1,84 @@ -/// +import { BaseView } from "./BaseView"; -module TK.SpaceTac.UI { - export enum AssetLoadingRange { - NONE, - MENU, - BATTLE, - CAMPAIGN - } - - /** - * Loader of all game assets - */ - export class AssetLoading extends BaseView { - private static loaded = AssetLoadingRange.NONE - private required = AssetLoadingRange.NONE - - /** - * Check if an asset range is loaded - */ - static isRangeLoaded(game: Phaser.Game, range: AssetLoadingRange): boolean { - if (range > AssetLoading.loaded) { - return false; - } else { - return true; - } - } - - init(data: any) { - super.init(data); - - this.required = data ? data.range : AssetLoadingRange.NONE; - } - - preload() { - let bg = this.add.image(this.getX(0.5) - 317, this.getY(0.5) - 105, "preload-background"); - bg.setOrigin(0); - let bar = this.add.image(this.getX(0.5) - 317, this.getY(0.5) - 105, "preload-bar"); - bar.setOrigin(0); - let mask = this.make.graphics({ x: bar.x, y: bar.y, add: false }); - mask.fillStyle(0xffffff); - bar.setMask(new Phaser.Display.Masks.GeometryMask(this, mask)); - - this.load.on('progress', (value: number) => { - mask.clear(); - mask.fillRect(0, 0, value * bar.width, bar.height); - }); - - let text = this.add.text(this.getX(0.5), this.getY(0.5) - 74, "... Loading ...", { font: "normal 36pt SpaceTac", fill: "#dbeff9" }); - text.setOrigin(0.5); - - if (this.required >= AssetLoadingRange.MENU && AssetLoading.loaded < AssetLoadingRange.MENU) { - console.log("Loading menu assets"); - this.load.pack("stage1", `assets/pack1.json?t=${Date.now()}`); - } - - if (this.required >= AssetLoadingRange.BATTLE && AssetLoading.loaded < AssetLoadingRange.BATTLE) { - console.log("Loading battle assets"); - this.load.pack("stage2", `assets/pack2.json?t=${Date.now()}`); - } - - if (this.required >= AssetLoadingRange.CAMPAIGN && AssetLoading.loaded < AssetLoadingRange.CAMPAIGN) { - console.log("Loading campaign assets"); - this.load.pack("stage3", `assets/pack3.json?t=${Date.now()}`); - } - } - - create() { - super.create(); - - AssetLoading.loaded = Math.max(AssetLoading.loaded, this.required); - this.backToRouter(); - } - - static getKey(path: string): string { - return path.replace(/\//g, "-").replace(/\.[a-z0-9]+$/, ''); - } - - loadSheet(path: string, frame_width: number, frame_height = frame_width) { - this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, { - frameWidth: frame_width, - frameHeight: frame_height, - }); - } - } +export enum AssetLoadingRange { + NONE, + MENU, + BATTLE, + CAMPAIGN +} + +/** + * Loader of all game assets + */ +export class AssetLoading extends BaseView { + private static loaded = AssetLoadingRange.NONE + private required = AssetLoadingRange.NONE + + /** + * Check if an asset range is loaded + */ + static isRangeLoaded(game: Phaser.Game, range: AssetLoadingRange): boolean { + if (range > AssetLoading.loaded) { + return false; + } else { + return true; + } + } + + init(data: any) { + super.init(data); + + this.required = data ? data.range : AssetLoadingRange.NONE; + } + + preload() { + let bg = this.add.image(this.getX(0.5) - 317, this.getY(0.5) - 105, "preload-background"); + bg.setOrigin(0); + let bar = this.add.image(this.getX(0.5) - 317, this.getY(0.5) - 105, "preload-bar"); + bar.setOrigin(0); + let mask = this.make.graphics({ x: bar.x, y: bar.y, add: false }); + mask.fillStyle(0xffffff); + bar.setMask(new Phaser.Display.Masks.GeometryMask(this, mask)); + + this.load.on('progress', (value: number) => { + mask.clear(); + mask.fillRect(0, 0, value * bar.width, bar.height); + }); + + let text = this.add.text(this.getX(0.5), this.getY(0.5) - 74, "... Loading ...", { font: "normal 36pt SpaceTac", fill: "#dbeff9" }); + text.setOrigin(0.5); + + if (this.required >= AssetLoadingRange.MENU && AssetLoading.loaded < AssetLoadingRange.MENU) { + console.log("Loading menu assets"); + this.load.pack("stage1", `assets/pack1.json?t=${Date.now()}`); + } + + if (this.required >= AssetLoadingRange.BATTLE && AssetLoading.loaded < AssetLoadingRange.BATTLE) { + console.log("Loading battle assets"); + this.load.pack("stage2", `assets/pack2.json?t=${Date.now()}`); + } + + if (this.required >= AssetLoadingRange.CAMPAIGN && AssetLoading.loaded < AssetLoadingRange.CAMPAIGN) { + console.log("Loading campaign assets"); + this.load.pack("stage3", `assets/pack3.json?t=${Date.now()}`); + } + } + + create() { + super.create(); + + AssetLoading.loaded = Math.max(AssetLoading.loaded, this.required); + this.backToRouter(); + } + + static getKey(path: string): string { + return path.replace(/\//g, "-").replace(/\.[a-z0-9]+$/, ''); + } + + loadSheet(path: string, frame_width: number, frame_height = frame_width) { + this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, { + frameWidth: frame_width, + frameHeight: frame_height, + }); + } } diff --git a/src/ui/BaseView.spec.ts b/src/ui/BaseView.spec.ts index 48aff67..74ff05d 100644 --- a/src/ui/BaseView.spec.ts +++ b/src/ui/BaseView.spec.ts @@ -1,19 +1,23 @@ -module TK.SpaceTac.UI.Specs { - testing("BaseView", test => { - let testgame = setupEmptyView(test); +import { testing } from "../common/Testing"; +import { BaseView } from "./BaseView"; +import { InputManager } from "./common/InputManager"; +import { Messages } from "./common/Messages"; +import { setupEmptyView } from "./TestGame"; - test.case("initializes variables", check => { - let view = testgame.view; +testing("BaseView", test => { + let testgame = setupEmptyView(test); - check.instance(view, BaseView, "view should be a BaseView"); - check.instance(view.messages, Messages, "view.messages should be a Messages"); - check.instance(view.inputs, InputManager, "view.inputs should be a InputManager"); - check.instance(view.audio, Audio, "view.audio should be an Audio"); + test.case("initializes variables", check => { + let view = testgame.view; - check.equals(view.getWidth(), 1920); - check.equals(view.getHeight(), 1080); - check.equals(view.getMidWidth(), 960); - check.equals(view.getMidHeight(), 540); - }); - }); -} + check.instance(view, BaseView, "view should be a BaseView"); + check.instance(view.messages, Messages, "view.messages should be a Messages"); + check.instance(view.inputs, InputManager, "view.inputs should be a InputManager"); + check.instance(view.audio, Audio, "view.audio should be an Audio"); + + check.equals(view.getWidth(), 1920); + check.equals(view.getHeight(), 1080); + check.equals(view.getMidWidth(), 960); + check.equals(view.getMidHeight(), 540); + }); +}); diff --git a/src/ui/BaseView.ts b/src/ui/BaseView.ts index b93cb8c..777c33b 100644 --- a/src/ui/BaseView.ts +++ b/src/ui/BaseView.ts @@ -1,269 +1,285 @@ -module TK.SpaceTac.UI { - /** - * Base class for all game views - */ - export class BaseView extends Phaser.Scene { - // Link to the root UI - gameui!: MainUI +import { Timer } from "../common/Timer" +import { classname, first } from "../common/Tools" +import { MainUI } from "../MainUI" +import { Connection } from "../multi/Connection" +import { FakeRemoteStorage, ParseRemoteStorage } from "../multi/RemoteStorage" +import { AssetLoading } from "./AssetLoading" +import { Animations } from "./common/Animations" +import { AudioManager } from "./common/AudioManager" +import { InputManager } from "./common/InputManager" +import { Messages } from "./common/Messages" +import { Tooltip } from "./common/Tooltip" +import { UIContainer } from "./common/UIContainer" +import { UIDialog } from "./common/UIDialog" +import { UIImage } from "./common/UIImage" +import { UIParticles } from "./common/UIParticles" +import { IBounded } from "./common/UITools" +import { OptionsDialog } from "./options/OptionsDialog" - // Message notifications - messages_layer!: UIContainer - messages!: Messages +/** + * Base class for all game views + */ +export class BaseView extends Phaser.Scene { + // Link to the root UI + gameui!: MainUI - // Audio system - audio!: Audio + // Message notifications + messages_layer!: UIContainer + messages!: Messages - // Input and key bindings - inputs!: InputManager + // Audio system + audio!: AudioManager - // Animations - animations!: Animations - particles!: UIParticles + // Input and key bindings + inputs!: InputManager - // Timing - timer!: Timer + // Animations + animations!: Animations + particles!: UIParticles - // Tooltip - tooltip_layer!: UIContainer - tooltip!: Tooltip + // Timing + timer!: Timer - // Layers - layers!: UIContainer + // Tooltip + tooltip_layer!: UIContainer + tooltip!: Tooltip - // Modal dialogs - dialogs_layer!: UIContainer - dialogs_opened: UIDialog[] = [] + // Layers + layers!: UIContainer - // Verbose debug output - debug = false + // Modal dialogs + dialogs_layer!: UIContainer + dialogs_opened: UIDialog[] = [] - // Get the size of display - getWidth(): number { - return 1920; - } - getHeight(): number { - return 1080; - } - getMidWidth(): number { - return 960; - } - getMidHeight(): number { - return 540; - } + // Verbose debug output + debug = false - init(data: object) { - console.log(`Starting scene ${classname(this)}`); + // Get the size of display + getWidth(): number { + return 1920; + } + getHeight(): number { + return 1080; + } + getMidWidth(): number { + return 960; + } + getMidHeight(): number { + return 540; + } - this.gameui = this.sys.game; - this.timer = new Timer(this.gameui.isTesting); - this.animations = new Animations(this.tweens); - this.particles = new UIParticles(this); - this.inputs = new InputManager(this); - this.audio = this.gameui.audio; - this.debug = this.gameui.debug; + init(data: object) { + console.log(`Starting scene ${classname(this)}`); - this.input.setDefaultCursor("url(cursors/standard.cur), pointer"); + this.gameui = this.sys.game; + this.timer = new Timer(this.gameui.isTesting); + this.animations = new Animations(this.tweens); + this.particles = new UIParticles(this); + this.inputs = new InputManager(this); + this.audio = this.gameui.audio; + this.debug = this.gameui.debug; - this.events.once("shutdown", () => this.shutdown()); - } + this.input.setDefaultCursor("url(cursors/standard.cur), pointer"); - shutdown() { - console.log(`Shutting down scene ${classname(this)}`); + this.events.once("shutdown", () => this.shutdown()); + } - this.inputs.destroy(); - this.audio.stopMusic(); - this.timer.cancelAll(true); - } + shutdown() { + console.log(`Shutting down scene ${classname(this)}`); - create() { - // Layers - this.layers = new UIContainer(this); - this.add.existing(this.layers); - this.layers.setName("View layers"); - this.dialogs_layer = new UIContainer(this); - this.dialogs_layer.setName("Dialogs layer"); - this.add.existing(this.dialogs_layer); - this.tooltip_layer = new UIContainer(this); - this.tooltip_layer.setName("Tooltip layer"); - this.add.existing(this.tooltip_layer); - this.tooltip = new Tooltip(this); - this.messages_layer = new UIContainer(this); - this.messages_layer.setName("Messages layer"); - this.add.existing(this.messages_layer); - this.messages = new Messages(this); - this.dialogs_opened = []; + this.inputs.destroy(); + this.audio.stopMusic(); + this.timer.cancelAll(true); + } - // Browser console variable (for debugging purpose) - if (typeof window != "undefined") { - let session = this.gameui.session; - if (session) { - (window).universe = session.universe; - (window).player = session.player; - (window).battle = session.player.getBattle(); - (window).view = this; - } - } - } + create() { + // Layers + this.layers = new UIContainer(this); + this.add.existing(this.layers); + this.layers.setName("View layers"); + this.dialogs_layer = new UIContainer(this); + this.dialogs_layer.setName("Dialogs layer"); + this.add.existing(this.dialogs_layer); + this.tooltip_layer = new UIContainer(this); + this.tooltip_layer.setName("Tooltip layer"); + this.add.existing(this.tooltip_layer); + this.tooltip = new Tooltip(this); + this.messages_layer = new UIContainer(this); + this.messages_layer.setName("Messages layer"); + this.add.existing(this.messages_layer); + this.messages = new Messages(this); + this.dialogs_opened = []; - get options() { - return this.gameui.options; - } - get session() { - return this.gameui.session; - } - - /** - * Go back to the router state - */ - backToRouter() { - this.scene.start('router'); - } - - /** - * Get or create a layer in the view, by its name - */ - getLayer(name: string): UIContainer { - let layer = this.layers.getByName(name); - if (layer && layer instanceof UIContainer) { - return layer; - } else { - let layer = new UIContainer(this); - layer.setName(name); - this.layers.add(layer); - return layer; - } - } - - /** - * Get proportional locations on screen - */ - getScaling(): number { - return this.gameui.scaling; - } - getX(propx: number, scaled = true): number { - return propx * 1920 * (scaled ? this.getScaling() : 1); - } - getY(propy: number, scaled = true): number { - return propy * 1080 * (scaled ? this.getScaling() : 1); - } - - /** - * Get a network connection to the backend server - */ - getConnection(): Multi.Connection { - let device_id = this.gameui.getDeviceId(); - if (device_id) { - return new Multi.Connection(device_id, new Multi.ParseRemoteStorage()); - } else { - // TODO Should warn the user ! - return new Multi.Connection("fake", new Multi.FakeRemoteStorage()); - } - } - - /** - * Auto-save current session to cloud - * - * This may be called at key points during the gameplay - */ - autoSave(): void { - let session = this.gameui.session; - if (session.primary) { - let connection = this.getConnection(); - connection.publish(session, session.getDescription()) - .then(() => this.messages.addMessage("Auto-saved to cloud")) - .catch(console.error) - //.catch(() => this.messages.addMessage("Error saving game to cloud")); - } - } - - /** - * Open options dialog - */ - showOptions(credits = false): void { - new OptionsDialog(this, credits); - } - - /** - * Set a value in localStorage, if available - */ - setStorage(key: string, value: string): void { - if (typeof localStorage != "undefined") { - localStorage.setItem("spacetac-" + key, value); - } - } - - /** - * Get a value from localStorage - */ - getStorage(key: string): string | null { - if (typeof localStorage != "undefined") { - return localStorage.getItem("spacetac-" + key); - } else { - return null; - } - } - - /** - * Check if the mouse is inside a given area - */ - isMouseInside(area: IBounded): boolean { - let pos = this.input.activePointer.position; - return pos.x >= area.x && pos.x < area.x + area.width && pos.y >= area.y && pos.y < area.y + area.height; - } - - /** - * Get a new image from an atlas name - */ - newImage(name: string, x = 0, y = 0): UIImage { - let info = this.getImageInfo(name); - let result = this.add.image(x, y, info.key, info.frame); - result.name = name; - return result; - } - - /** - * Update an image from an atlas name - */ - changeImage(image: UIImage, name: string): void { - let info = this.getImageInfo(name); - image.setName(name); - image.setTexture(info.key, info.frame); - } - - /** - * Get an image from atlases - */ - getImageInfo(name: string): { key: string, frame: number | string, exists: boolean } { - if (this.textures.exists(name)) { - return { key: name, frame: 0, exists: true }; - } else { - for (let j = 1; j <= 3; j++) { - let i = 1; - while (this.textures.exists(`atlas${j}-${i}`)) { - let frames = this.textures.get(`atlas${j}-${i}`).getFrameNames(); - let frame = first(frames, frame => AssetLoading.getKey(frame) == `data-stage${j}-image-${name}`); - if (frame) { - return { key: `atlas${j}-${i}`, frame: frame, exists: true }; - } - i++; - } - } - return { key: `-missing-${name}`, frame: 0, exists: false }; - } - } - - /** - * Returns the first image found in atlases - */ - getFirstImage(...names: string[]): string { - return first(names, name => this.getImageInfo(name).key.substr(0, 9) != '-missing-') || names[names.length - 1]; - } - - /** - * Check if the scene is paused - */ - isPaused(): boolean { - return this.time.paused; - } + // Browser console variable (for debugging purpose) + if (typeof window != "undefined") { + let session = this.gameui.session; + if (session) { + (window).universe = session.universe; + (window).player = session.player; + (window).battle = session.player.getBattle(); + (window).view = this; + } } + } + + get options() { + return this.gameui.options; + } + get session() { + return this.gameui.session; + } + + /** + * Go back to the router state + */ + backToRouter() { + this.scene.start('router'); + } + + /** + * Get or create a layer in the view, by its name + */ + getLayer(name: string): UIContainer { + let layer = this.layers.getByName(name); + if (layer && layer instanceof UIContainer) { + return layer; + } else { + let layer = new UIContainer(this); + layer.setName(name); + this.layers.add(layer); + return layer; + } + } + + /** + * Get proportional locations on screen + */ + getScaling(): number { + return this.gameui.scaling; + } + getX(propx: number, scaled = true): number { + return propx * 1920 * (scaled ? this.getScaling() : 1); + } + getY(propy: number, scaled = true): number { + return propy * 1080 * (scaled ? this.getScaling() : 1); + } + + /** + * Get a network connection to the backend server + */ + getConnection(): Connection { + let device_id = this.gameui.getDeviceId(); + if (device_id) { + return new Connection(device_id, new ParseRemoteStorage()); + } else { + // TODO Should warn the user ! + return new Connection("fake", new FakeRemoteStorage()); + } + } + + /** + * Auto-save current session to cloud + * + * This may be called at key points during the gameplay + */ + autoSave(): void { + let session = this.gameui.session; + if (session.primary) { + let connection = this.getConnection(); + connection.publish(session, session.getDescription()) + .then(() => this.messages.addMessage("Auto-saved to cloud")) + .catch(console.error) + //.catch(() => this.messages.addMessage("Error saving game to cloud")); + } + } + + /** + * Open options dialog + */ + showOptions(credits = false): void { + new OptionsDialog(this, credits); + } + + /** + * Set a value in localStorage, if available + */ + setStorage(key: string, value: string): void { + if (typeof localStorage != "undefined") { + localStorage.setItem("spacetac-" + key, value); + } + } + + /** + * Get a value from localStorage + */ + getStorage(key: string): string | null { + if (typeof localStorage != "undefined") { + return localStorage.getItem("spacetac-" + key); + } else { + return null; + } + } + + /** + * Check if the mouse is inside a given area + */ + isMouseInside(area: IBounded): boolean { + let pos = this.input.activePointer.position; + return pos.x >= area.x && pos.x < area.x + area.width && pos.y >= area.y && pos.y < area.y + area.height; + } + + /** + * Get a new image from an atlas name + */ + newImage(name: string, x = 0, y = 0): UIImage { + let info = this.getImageInfo(name); + let result = this.add.image(x, y, info.key, info.frame); + result.name = name; + return result; + } + + /** + * Update an image from an atlas name + */ + changeImage(image: UIImage, name: string): void { + let info = this.getImageInfo(name); + image.setName(name); + image.setTexture(info.key, info.frame); + } + + /** + * Get an image from atlases + */ + getImageInfo(name: string): { key: string, frame: number | string, exists: boolean } { + if (this.textures.exists(name)) { + return { key: name, frame: 0, exists: true }; + } else { + for (let j = 1; j <= 3; j++) { + let i = 1; + while (this.textures.exists(`atlas${j}-${i}`)) { + let frames = this.textures.get(`atlas${j}-${i}`).getFrameNames(); + let frame = first(frames, frame => AssetLoading.getKey(frame) == `data-stage${j}-image-${name}`); + if (frame) { + return { key: `atlas${j}-${i}`, frame: frame, exists: true }; + } + i++; + } + } + return { key: `-missing-${name}`, frame: 0, exists: false }; + } + } + + /** + * Returns the first image found in atlases + */ + getFirstImage(...names: string[]): string { + return first(names, name => this.getImageInfo(name).key.substr(0, 9) != '-missing-') || names[names.length - 1]; + } + + /** + * Check if the scene is paused + */ + isPaused(): boolean { + return this.time.paused; + } } diff --git a/src/ui/Boot.ts b/src/ui/Boot.ts index c69fc80..2d552b1 100644 --- a/src/ui/Boot.ts +++ b/src/ui/Boot.ts @@ -1,19 +1,17 @@ -module TK.SpaceTac.UI { - /** - * First view to boot. - * - * It is responsible to prepare the screen, and the asset loading. - */ - export class Boot extends Phaser.Scene { - preload() { - this.load.image("preload-background", "images/preload/bar-background.png"); - this.load.image("preload-bar", "images/preload/bar-content.png"); - } +/** + * First view to boot. + * + * It is responsible to prepare the screen, and the asset loading. + */ +export class Boot extends Phaser.Scene { + preload() { + this.load.image("preload-background", "images/preload/bar-background.png"); + this.load.image("preload-bar", "images/preload/bar-content.png"); + } - create() { - this.add.image(643, 435, "preload-background"); + create() { + this.add.image(643, 435, "preload-background"); - this.scene.start("router"); - } - } + this.scene.start("router"); + } } diff --git a/src/ui/Router.spec.ts b/src/ui/Router.spec.ts index dfdea96..f407df7 100644 --- a/src/ui/Router.spec.ts +++ b/src/ui/Router.spec.ts @@ -1,10 +1,13 @@ -module TK.SpaceTac.UI.Specs { - testing("Router", test => { - let testgame = setupSingleView(test, () => [new Router({}), {}]); +import { Router } from "parse"; +import { testing } from "../common/Testing"; +import { setupSingleView } from "./TestGame"; + +testing("Router", test => { + let testgame = setupSingleView(test, () => [new Router({}), {}]); + + test.case("loads correctly", check => { + check.instance(testgame.ui.scene.scenes[0], Router, "active scene should be Router"); + // TODO test routing + }); +}); - test.case("loads correctly", check => { - check.instance(testgame.ui.scene.scenes[0], Router, "active scene should be Router"); - // TODO test routing - }); - }); -} diff --git a/src/ui/Router.ts b/src/ui/Router.ts index 40da503..afdce41 100644 --- a/src/ui/Router.ts +++ b/src/ui/Router.ts @@ -1,44 +1,45 @@ -module TK.SpaceTac.UI { - /** - * Router to other views - * - * It will go to the expected view, by examining the current game session. - * - * If needed, it will go back to the asset loading state. - */ - export class Router extends BaseView { - create() { - super.create(); +import { AssetLoading, AssetLoadingRange } from "./AssetLoading"; +import { BaseView } from "./BaseView"; - let session = this.session; +/** + * Router to other views + * + * It will go to the expected view, by examining the current game session. + * + * If needed, it will go back to the asset loading state. + */ +export class Router extends BaseView { + create() { + super.create(); - if (session.getBattle()) { - // A battle is raging, go to it - this.goToState("battle", AssetLoadingRange.BATTLE, { player: session.player, battle: session.getBattle() }); - } else if (session.hasUniverse()) { - // Campaign mode - if (session.isFleetCreated()) { - // Go to the universe map - this.goToState("universe", AssetLoadingRange.CAMPAIGN, { player: session.player, universe: session.universe }); - } else if (session.isIntroViewed()) { - // Build initial fleet - this.goToState("creation", AssetLoadingRange.CAMPAIGN); - } else { - // Show intro - this.goToState("intro", AssetLoadingRange.CAMPAIGN); - } - } else { - // No battle, no campaign, go back to menu to decide what to do - this.goToState("mainmenu", AssetLoadingRange.MENU); - } - } + let session = this.session; - goToState(name: string, asset_range: AssetLoadingRange, data?: object) { - if (AssetLoading.isRangeLoaded(this.game, asset_range)) { - this.scene.start(name, data); - } else { - this.scene.start("loading", { range: asset_range }); - } - } + if (session.getBattle()) { + // A battle is raging, go to it + this.goToState("battle", AssetLoadingRange.BATTLE, { player: session.player, battle: session.getBattle() }); + } else if (session.hasUniverse()) { + // Campaign mode + if (session.isFleetCreated()) { + // Go to the universe map + this.goToState("universe", AssetLoadingRange.CAMPAIGN, { player: session.player, universe: session.universe }); + } else if (session.isIntroViewed()) { + // Build initial fleet + this.goToState("creation", AssetLoadingRange.CAMPAIGN); + } else { + // Show intro + this.goToState("intro", AssetLoadingRange.CAMPAIGN); + } + } else { + // No battle, no campaign, go back to menu to decide what to do + this.goToState("mainmenu", AssetLoadingRange.MENU); } + } + + goToState(name: string, asset_range: AssetLoadingRange, data?: object) { + if (AssetLoading.isRangeLoaded(this.game, asset_range)) { + this.scene.start(name, data); + } else { + this.scene.start("loading", { range: asset_range }); + } + } } diff --git a/src/ui/TestGame.ts b/src/ui/TestGame.ts index 7b8ef2b..44d796d 100644 --- a/src/ui/TestGame.ts +++ b/src/ui/TestGame.ts @@ -1,173 +1,186 @@ -/// +import { MainUI } from ".." +import { RandomGenerator } from "../common/RandomGenerator" +import { FakeClock, TestContext, TestSuite } from "../common/Testing" +import { bound, nn } from "../common/Tools" +import { Battle } from "../core/Battle" +import { GameSession } from "../core/GameSession" +import { Player } from "../core/Player" +import { Connection } from "../multi/Connection" +import { FakeRemoteStorage } from "../multi/RemoteStorage" +import { BaseView } from "./BaseView" +import { BattleView } from "./battle/BattleView" +import { UIButton } from "./common/UIButton" +import { UIContainer } from "./common/UIContainer" +import { UIImage } from "./common/UIImage" +import { UIText } from "./common/UIText" +import { UniverseMapView } from "./map/UniverseMapView" -module TK.SpaceTac.UI.Specs { - /** - * Class to hold references to test objects (used as singleton in "testing" blocks) - * - * Attributes should only be accessed from inside corresponding "test.case" blocks (they are initialized by the setup). - */ - export class TestGame { - check!: TestContext - ui!: MainUI - view!: T - multistorage!: Multi.FakeRemoteStorage - clock: FakeClock - time = 0 +/** + * Class to hold references to test objects (used as singleton in "testing" blocks) + * + * Attributes should only be accessed from inside corresponding "test.case" blocks (they are initialized by the setup). + */ +export class TestGame { + check!: TestContext + ui!: MainUI + view!: T + multistorage!: FakeRemoteStorage + clock: FakeClock + time = 0 - constructor(test: TestSuite) { - this.clock = test.clock(); - } + constructor(test: TestSuite) { + this.clock = test.clock(); + } - /** - * Advance the time in the view and fake testing clock - */ - clockForward(milliseconds: number) { - this.time += milliseconds; - this.clock.forward(milliseconds); - this.ui.headlessStep(this.time, milliseconds); - } - } - - /** - * Setup a headless test UI, with a single view started. - */ - export function setupSingleView(test: TestSuite, buildView: () => [T, object]) { - let testgame = new TestGame(test); - - test.asetup(() => new Promise((resolve, reject) => { - let check = new TestContext(); // TODO Should be taken from test suite - check.patch(console, "log", null); - check.patch(console, "warn", null); - - testgame.ui = new MainUI(true); - testgame.check = check; - - let [scene, scenedata] = buildView(); - - if (scene instanceof BaseView) { - testgame.multistorage = new Multi.FakeRemoteStorage(); - let connection = new Multi.Connection(RandomGenerator.global.id(12), testgame.multistorage); - check.patch(scene as BaseView, "getConnection", () => connection); - } - - let orig_create = bound(scene, "create"); - check.patch(scene, "create", () => { - orig_create(); - resolve(); - }); - - testgame.ui.scene.add("test", scene, true, scenedata); - - testgame.view = scene; - }), () => new Promise((resolve) => { - testgame.ui.events.on("destroy", () => resolve()); - testgame.ui.destroy(true); - })); - - return testgame; - } - - /** - * Test setup of an empty BaseView - */ - export function setupEmptyView(test: TestSuite): TestGame { - return setupSingleView(test, () => { - return [new BaseView({}), {}]; - }); - } - - /** - * Test setup of a battleview bound to a battle, to be called inside a "describe" block. - */ - export function setupBattleview(test: TestSuite): TestGame { - return setupSingleView(test, () => { - let view = new BattleView({}); - view.splash = false; - - let battle = Battle.newQuickRandom(); - let player = new Player(); - nn(battle.playing_ship).fleet.setPlayer(player); - - return [view, { player, battle }]; - }); - } - - /** - * Test setup of a mapview bound to a universe, to be called inside a "describe" block. - */ - export function setupMapview(test: TestSuite): TestGame { - return setupSingleView(test, () => { - let mapview = new UniverseMapView({}); - let session = new GameSession(); - session.startNewGame(); - - return [mapview, { universe: session.universe, player: session.player }]; - }); - } - - /** - * Crawn through the children of a node - */ - export function crawlChildren(node: UIContainer, recursive: boolean, callback: (child: any) => void): void { - node.list.forEach(child => { - callback(child); - if (recursive && child instanceof UIContainer) { - crawlChildren(child, true, callback); - } - }); - } - - /** - * Collect all image codes in a node - */ - export function collectImages(node: UIContainer, recursive = true): (string | null)[] { - let result: (string | null)[] = []; - crawlChildren(node, recursive, child => { - if (child instanceof UIImage) { - result.push(child.name || null); - } - }); - return result; - } - - /** - * Collect all texts in a node - */ - export function collectTexts(node: UIContainer, recursive = true): (string | null)[] { - let result: (string | null)[] = []; - crawlChildren(node, recursive, child => { - if (child instanceof UIText) { - result.push(child.text || null); - } - }); - return result; - } - - /** - * Check a given text node - */ - export function checkText(check: TestContext, node: any, content: string): void { - if (check.instance(node, UIText, "node should be an UIText")) { - check.equals(node.text, content); - } - } - - /** - * Check a simulation of a tweened property - */ - export function checkTween(game: TestGame, obj: T, property: P, expected: number[]): void { - let tweendata = game.view.animations.simulate(obj, property, expected.length); - game.check.equals(tweendata.length, expected.length, "number of points"); - expected.forEach((value, idx) => { - game.check.nears(tweendata[idx], value, undefined, `point ${idx}`); - }); - } - - /** - * Simulate a click on a button - */ - export function testClick(button: UIButton): void { - button.emit("pointerdown"); - button.emit("pointerup"); - } + /** + * Advance the time in the view and fake testing clock + */ + clockForward(milliseconds: number) { + this.time += milliseconds; + this.clock.forward(milliseconds); + this.ui.headlessStep(this.time, milliseconds); + } +} + +/** + * Setup a headless test UI, with a single view started. + */ +export function setupSingleView(test: TestSuite, buildView: () => [T, object]) { + let testgame = new TestGame(test); + + test.asetup(() => new Promise((resolve, reject) => { + let check = new TestContext(); // TODO Should be taken from test suite + check.patch(console, "log", null); + check.patch(console, "warn", null); + + testgame.ui = new MainUI(true); + testgame.check = check; + + let [scene, scenedata] = buildView(); + + if (scene instanceof BaseView) { + testgame.multistorage = new FakeRemoteStorage(); + let connection = new Connection(RandomGenerator.global.id(12), testgame.multistorage); + check.patch(scene as BaseView, "getConnection", () => connection); + } + + let orig_create = bound(scene, "create"); + check.patch(scene, "create", () => { + orig_create(); + resolve(); + }); + + testgame.ui.scene.add("test", scene, true, scenedata); + + testgame.view = scene; + }), () => new Promise((resolve) => { + testgame.ui.events.on("destroy", () => resolve()); + testgame.ui.destroy(true); + })); + + return testgame; +} + +/** + * Test setup of an empty BaseView + */ +export function setupEmptyView(test: TestSuite): TestGame { + return setupSingleView(test, () => { + return [new BaseView({}), {}]; + }); +} + +/** + * Test setup of a battleview bound to a battle, to be called inside a "describe" block. + */ +export function setupBattleview(test: TestSuite): TestGame { + return setupSingleView(test, () => { + let view = new BattleView({}); + view.splash = false; + + let battle = Battle.newQuickRandom(); + let player = new Player(); + nn(battle.playing_ship).fleet.setPlayer(player); + + return [view, { player, battle }]; + }); +} + +/** + * Test setup of a mapview bound to a universe, to be called inside a "describe" block. + */ +export function setupMapview(test: TestSuite): TestGame { + return setupSingleView(test, () => { + let mapview = new UniverseMapView({}); + let session = new GameSession(); + session.startNewGame(); + + return [mapview, { universe: session.universe, player: session.player }]; + }); +} + +/** + * Crawn through the children of a node + */ +export function crawlChildren(node: UIContainer, recursive: boolean, callback: (child: any) => void): void { + node.list.forEach(child => { + callback(child); + if (recursive && child instanceof UIContainer) { + crawlChildren(child, true, callback); + } + }); +} + +/** + * Collect all image codes in a node + */ +export function collectImages(node: UIContainer, recursive = true): (string | null)[] { + let result: (string | null)[] = []; + crawlChildren(node, recursive, child => { + if (child instanceof UIImage) { + result.push(child.name || null); + } + }); + return result; +} + +/** + * Collect all texts in a node + */ +export function collectTexts(node: UIContainer, recursive = true): (string | null)[] { + let result: (string | null)[] = []; + crawlChildren(node, recursive, child => { + if (child instanceof UIText) { + result.push(child.text || null); + } + }); + return result; +} + +/** + * Check a given text node + */ +export function checkText(check: TestContext, node: any, content: string): void { + if (check.instance(node, UIText, "node should be an UIText")) { + check.equals(node.text, content); + } +} + +/** + * Check a simulation of a tweened property + */ +export function checkTween(game: TestGame, obj: T, property: P, expected: number[]): void { + let tweendata = game.view.animations.simulate(obj, property, expected.length); + game.check.equals(tweendata.length, expected.length, "number of points"); + expected.forEach((value, idx) => { + game.check.nears(tweendata[idx], value, undefined, `point ${idx}`); + }); +} + +/** + * Simulate a click on a button + */ +export function testClick(button: UIButton): void { + button.emit("pointerdown"); + button.emit("pointerup"); } diff --git a/src/ui/battle/ActionBar.spec.ts b/src/ui/battle/ActionBar.spec.ts index 43fe0cb..bc181fe 100644 --- a/src/ui/battle/ActionBar.spec.ts +++ b/src/ui/battle/ActionBar.spec.ts @@ -1,82 +1,90 @@ -module TK.SpaceTac.UI.Specs { - testing("ActionBar", test => { - let testgame = setupBattleview(test); +import { NAMESPACE } from "../.."; +import { testing } from "../../common/Testing"; +import { duplicate } from "../../common/Tools"; +import { ShipAttributeDiff } from "../../core/diffs/ShipAttributeDiff"; +import { Player } from "../../core/Player"; +import { Ship } from "../../core/Ship"; +import { TestTools } from "../../core/TestTools"; +import { UIImage } from "../common/UIImage"; +import { setupBattleview } from "../TestGame"; - test.case("lists available actions for selected ship", check => { - var bar = testgame.view.action_bar; +testing("ActionBar", test => { + let testgame = setupBattleview(test); - // Ship not owned by current battleview player - var ship = new Ship(); - bar.setShip(ship); - check.equals(bar.action_icons.length, 0); + test.case("lists available actions for selected ship", check => { + var bar = testgame.view.action_bar; - // Ship with no equipment (only endturn action) - let player = new Player(); - ship.fleet.setPlayer(player); - testgame.view.player = player; - bar.setShip(ship); - check.equals(bar.action_icons.length, 1); - check.equals(bar.action_icons[0].action.code, "endturn"); + // Ship not owned by current battleview player + var ship = new Ship(); + bar.setShip(ship); + check.equals(bar.action_icons.length, 0); - // Add an engine, with move action - TestTools.addEngine(ship, 50); - bar.setShip(ship); - check.equals(bar.action_icons.length, 2); - check.equals(bar.action_icons[0].action.code, "move"); + // Ship with no equipment (only endturn action) + let player = new Player(); + ship.fleet.setPlayer(player); + testgame.view.player = player; + bar.setShip(ship); + check.equals(bar.action_icons.length, 1); + check.equals(bar.action_icons[0].action.code, "endturn"); - // Add a weapon, with fire action - TestTools.addWeapon(ship, 10, 1, 100); - bar.setShip(ship); - check.equals(bar.action_icons.length, 3); - check.equals(bar.action_icons[1].action.code, "weapon"); - }); + // Add an engine, with move action + TestTools.addEngine(ship, 50); + bar.setShip(ship); + check.equals(bar.action_icons.length, 2); + check.equals(bar.action_icons[0].action.code, "move"); - test.case("updates power points display", check => { - let bar = testgame.view.action_bar; + // Add a weapon, with fire action + TestTools.addWeapon(ship, 10, 1, 100); + bar.setShip(ship); + check.equals(bar.action_icons.length, 3); + check.equals(bar.action_icons[1].action.code, "weapon"); + }); - function checkpoints(desc: string, available = 0, using = 0, used = 0) { - check.in(desc, check => { - check.same(bar.power_icons.length, available + using + used, "icon count"); - bar.power_icons.list.forEach((child, idx) => { - if (check.instance(child, UIImage, `${idx} icon should be an image`)) { - if (idx < available) { - check.equals(child.name, "battle-actionbar-power-available", `icon ${idx}`); - } else if (idx < available + using) { - check.equals(child.name, "battle-actionbar-power-move", `icon ${idx}`); - } else { - check.equals(child.name, "battle-actionbar-power-used", `icon ${idx}`); - } - } - }); - }); + test.case("updates power points display", check => { + let bar = testgame.view.action_bar; + + function checkpoints(desc: string, available = 0, using = 0, used = 0) { + check.in(desc, check => { + check.same(bar.power_icons.length, available + using + used, "icon count"); + bar.power_icons.list.forEach((child, idx) => { + if (check.instance(child, UIImage, `${idx} icon should be an image`)) { + if (idx < available) { + check.equals(child.name, "battle-actionbar-power-available", `icon ${idx}`); + } else if (idx < available + using) { + check.equals(child.name, "battle-actionbar-power-move", `icon ${idx}`); + } else { + check.equals(child.name, "battle-actionbar-power-used", `icon ${idx}`); } - - // not owned ship - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, 8); - bar.setShip(ship); - checkpoints("not owned ship"); - - // owned ship - testgame.view.player.fleet.addShip(ship); - testgame.view.battle.ships.add(duplicate(ship, TK.SpaceTac)); - testgame.view.actual_battle.ships.add(ship); - bar.setShip(ship); - checkpoints("owned ship", 8); - - // used points - testgame.view.actual_battle.applyDiffs(ship.getValueDiffs("power", 6)); - testgame.view.log_processor.processPending(); - checkpoints("2 points used", 6, 0, 2); - - // using points - bar.updatePower(5); - checkpoints("5 points in targetting", 1, 5, 2); - - // decrease - testgame.view.actual_battle.applyDiffs([new ShipAttributeDiff(ship, "power_capacity", { limit: 3 }, {})]); - testgame.view.log_processor.processPending(); - checkpoints("limit to 3", 3); + } }); - }); -} + }); + } + + // not owned ship + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, 8); + bar.setShip(ship); + checkpoints("not owned ship"); + + // owned ship + testgame.view.player.fleet.addShip(ship); + testgame.view.battle.ships.add(duplicate(ship, NAMESPACE)); + testgame.view.actual_battle.ships.add(ship); + bar.setShip(ship); + checkpoints("owned ship", 8); + + // used points + testgame.view.actual_battle.applyDiffs(ship.getValueDiffs("power", 6)); + testgame.view.log_processor.processPending(); + checkpoints("2 points used", 6, 0, 2); + + // using points + bar.updatePower(5); + checkpoints("5 points in targetting", 1, 5, 2); + + // decrease + testgame.view.actual_battle.applyDiffs([new ShipAttributeDiff(ship, "power_capacity", { limit: 3 }, {})]); + testgame.view.log_processor.processPending(); + checkpoints("limit to 3", 3); + }); +}); diff --git a/src/ui/battle/ActionBar.ts b/src/ui/battle/ActionBar.ts index 0ff6e09..6143264 100644 --- a/src/ui/battle/ActionBar.ts +++ b/src/ui/battle/ActionBar.ts @@ -1,250 +1,265 @@ -/// +import { any, range } from "../../common/Tools" +import { BaseAction } from "../../core/actions/BaseAction" +import { BaseBattleShipDiff } from "../../core/diffs/BaseBattleDiff" +import { ShipActionToggleDiff } from "../../core/diffs/ShipActionToggleDiff" +import { ShipActionUsedDiff } from "../../core/diffs/ShipActionUsedDiff" +import { ShipAttributeDiff } from "../../core/diffs/ShipAttributeDiff" +import { ShipChangeDiff } from "../../core/diffs/ShipChangeDiff" +import { ShipCooldownDiff } from "../../core/diffs/ShipCooldownDiff" +import { ShipValueDiff } from "../../core/diffs/ShipValueDiff" +import { MoveFireResult } from "../../core/MoveFireSimulator" +import { Ship } from "../../core/Ship" +import { UIAwaiter } from "../common/UIAwaiter" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { destroyChildren } from "../common/UITools" +import { ActionIcon } from "./ActionIcon" +import { BattleView } from "./BattleView" -module TK.SpaceTac.UI { - /** - * Bar on the border of screen to display all available action icons - */ - export class ActionBar extends UIContainer { - // Link to the parent battleview - battleview: BattleView +/** + * Bar on the border of screen to display all available action icons + */ +export class ActionBar extends UIContainer { + // Link to the parent battleview + battleview: BattleView - // List of action icons - actions: UIContainer - action_icons: ActionIcon[] + // List of action icons + actions: UIContainer + action_icons: ActionIcon[] - // Power indicator - power: UIContainer - power_icons!: UIContainer + // Power indicator + power: UIContainer + power_icons!: UIContainer - // Indicator of interaction disabled - icon_waiting: UIAwaiter + // Indicator of interaction disabled + icon_waiting: UIAwaiter - // Current ship, whose actions are displayed - ship: Ship | null + // Current ship, whose actions are displayed + ship: Ship | null - // Interactivity - interactive = true; + // Interactivity + interactive = true; - // Create an empty action bar - constructor(battleview: BattleView) { - super(battleview); + // Create an empty action bar + constructor(battleview: BattleView) { + super(battleview); - this.battleview = battleview; - this.action_icons = []; - this.ship = null; + this.battleview = battleview; + this.action_icons = []; + this.ship = null; - battleview.layer_borders.add(this); + battleview.layer_borders.add(this); - let builder = new UIBuilder(battleview, this); + let builder = new UIBuilder(battleview, this); - // Background - let base = builder.image("battle-actionbar-background"); + // Background + let base = builder.image("battle-actionbar-background"); - // Group for actions - this.actions = builder.container("actions", 86, 6); - builder.in(this.actions).image("battle-actionbar-actions-background"); + // Group for actions + this.actions = builder.container("actions", 86, 6); + builder.in(this.actions).image("battle-actionbar-actions-background"); - // Power bar - this.power = builder.container("power", 1466, 0); - builder.in(this.power, builder => { - builder.image("battle-actionbar-power-background", 0, 6); - this.power_icons = builder.container("power icons", 50, 14); - }); + // Power bar + this.power = builder.container("power", 1466, 0); + builder.in(this.power, builder => { + builder.image("battle-actionbar-power-background", 0, 6); + this.power_icons = builder.container("power icons", 50, 14); + }); - // Playing ship - builder.image("battle-actionbar-ship", 1735); + // Playing ship + builder.image("battle-actionbar-ship", 1735); - // Waiting icon - this.icon_waiting = builder.awaiter(base.width / 2, base.height / 2, true, 0.5); + // Waiting icon + this.icon_waiting = builder.awaiter(base.width / 2, base.height / 2, true, 0.5); - // Options button - builder.button("battle-actionbar-button-menu", 0, 0, () => battleview.showOptions(), "Game options"); + // Options button + builder.button("battle-actionbar-button-menu", 0, 0, () => battleview.showOptions(), "Game options"); - // Log processing - battleview.log_processor.register(diff => { - if (!(diff instanceof BaseBattleShipDiff) || !this.ship || !this.ship.is(diff.ship_id)) { - return {}; - } - - if (diff instanceof ShipValueDiff && diff.code == "power") { - return { - background: async () => { - this.updatePower(); - this.action_icons.forEach(action => action.refresh()); - } - } - } else if (diff instanceof ShipAttributeDiff && diff.code == "power_capacity") { - return { - background: async () => this.updatePower() - } - } else if (diff instanceof ShipActionUsedDiff || diff instanceof ShipActionToggleDiff) { - return { - background: async () => this.action_icons.forEach(action => action.refresh()) - } - } else if (diff instanceof ShipCooldownDiff) { - return { - background: async () => { - let icons = this.action_icons.filter(icon => icon.action.is(diff.action)); - icons.forEach(icon => icon.refresh()); - } - } - } else if (diff instanceof ShipChangeDiff) { - return { - background: async () => { - this.setShip(null); - } - } - } else { - return {} - } - }); - - battleview.log_processor.watchForShipChange(ship => { - return { - background: async () => { - this.setShip(ship); - } - } - }); - this.setInteractivity(false); - } - - /** - * Check if an action is selected - */ - hasActionSelected(): boolean { - return any(this.action_icons, icon => icon.selected); - } - - /** - * Set the interactivity state - */ - setInteractivity(interactive: boolean) { - if (this.interactive != interactive) { - this.interactive = interactive; - - this.battleview.animations.setVisible(this.icon_waiting, !this.interactive, 100); - this.battleview.animations.setVisible(this.power, interactive, 100, 1, 0.3); - this.battleview.animations.setVisible(this.actions, interactive, 100, 1, 0.3); - } - } - - /** - * Called when an action shortcut key is pressed - */ - keyActionPressed(position: number) { - if (this.interactive) { - if (position < 0) { - this.action_icons[this.action_icons.length - 1].processClick(); - } else if (position < this.action_icons.length - 1) { - this.action_icons[position].processClick(); - } - } - } - - /** - * Remove all the action icons - */ - clearAll(): void { - this.action_icons.forEach(action => action.destroy()); - this.action_icons = []; - } - - /** - * Add an action icon - */ - addAction(ship: Ship, action: BaseAction): ActionIcon { - var icon = new ActionIcon(this, ship, action, this.action_icons.length); - icon.moveTo(this.actions, 74 + this.action_icons.length * 138, 58); - this.action_icons.push(icon); - - return icon; - } - - /** - * Update the power indicator - */ - updatePower(move_power = 0, fire_power = 0): void { - let power_capacity = this.ship ? this.ship.getAttribute("power_capacity") : 0; - let power_value = this.ship ? this.ship.getValue("power") : 0; - - let current_power = this.power_icons.length; - - if (current_power > power_capacity) { - destroyChildren(this.power_icons, power_capacity, current_power); - } else if (power_capacity > current_power) { - range(power_capacity - current_power).forEach(i => { - let x = (current_power + i) % 5; - let y = ((current_power + i) - x) / 5; - let image = new UIBuilder(this.battleview, this.power_icons).image("battle-actionbar-power-used", x * 43, y * 22); - }); - } - - let remaining_power = power_value - move_power - fire_power; - this.power_icons.list.forEach((obj, idx) => { - let img = obj; - if (idx < remaining_power) { - this.battleview.changeImage(img, "battle-actionbar-power-available"); - } else if (idx < remaining_power + move_power) { - this.battleview.changeImage(img, "battle-actionbar-power-move"); - } else if (idx < power_value) { - this.battleview.changeImage(img, "battle-actionbar-power-fire"); - } else { - this.battleview.changeImage(img, "battle-actionbar-power-used"); - } - }); - } - - /** - * Temporarily set current action power usage. - * - * When an action is selected, this will fade the icons not available after the action would be done. - * This will also highlight power usage in the power bar. - * - * *move_power* and *fire_power* is the consumption of currently selected action/target. - */ - updateSelectedActionPower(move_power: number, fire_power: number, action: BaseAction): void { - this.action_icons.forEach(icon => icon.refresh(action, move_power + fire_power)); - this.updatePower(move_power, fire_power); - } - - /** - * Temporarily set power status for a given move-fire simulation - */ - updateFromSimulation(action: BaseAction, simulation: MoveFireResult) { - if (simulation.complete) { - this.updateSelectedActionPower(simulation.total_move_ap, simulation.total_fire_ap, action); - } else { - this.updateSelectedActionPower(0, 0, action); - } - } - - /** - * Set the bar to display a given ship - */ - setShip(ship: Ship | null): void { - this.clearAll(); - - if (ship && this.battleview.player.is(ship.fleet.player) && ship.alive) { - ship.actions.listAll().forEach(action => this.addAction(ship, action)); - this.ship = ship; - } else { - this.ship = null; - } - - this.updatePower(); - } - - // Called by an action icon when the action is selected - actionStarted(): void { - } - - // Called by an action icon when the action has been applied - actionEnded(): void { - this.battleview.exitTargettingMode(); + // Log processing + battleview.log_processor.register(diff => { + if (!(diff instanceof BaseBattleShipDiff) || !this.ship || !this.ship.is(diff.ship_id)) { + return {}; + } + if (diff instanceof ShipValueDiff && diff.code == "power") { + return { + background: async () => { this.updatePower(); this.action_icons.forEach(action => action.refresh()); + } } + } else if (diff instanceof ShipAttributeDiff && diff.code == "power_capacity") { + return { + background: async () => this.updatePower() + } + } else if (diff instanceof ShipActionUsedDiff || diff instanceof ShipActionToggleDiff) { + return { + background: async () => this.action_icons.forEach(action => action.refresh()) + } + } else if (diff instanceof ShipCooldownDiff) { + return { + background: async () => { + let icons = this.action_icons.filter(icon => icon.action.is(diff.action)); + icons.forEach(icon => icon.refresh()); + } + } + } else if (diff instanceof ShipChangeDiff) { + return { + background: async () => { + this.setShip(null); + } + } + } else { + return {} + } + }); + + battleview.log_processor.watchForShipChange(ship => { + return { + background: async () => { + this.setShip(ship); + } + } + }); + this.setInteractivity(false); + } + + /** + * Check if an action is selected + */ + hasActionSelected(): boolean { + return any(this.action_icons, icon => icon.selected); + } + + /** + * Set the interactivity state + */ + setInteractivity(interactive: boolean) { + if (this.interactive != interactive) { + this.interactive = interactive; + + this.battleview.animations.setVisible(this.icon_waiting, !this.interactive, 100); + this.battleview.animations.setVisible(this.power, interactive, 100, 1, 0.3); + this.battleview.animations.setVisible(this.actions, interactive, 100, 1, 0.3); } + } + + /** + * Called when an action shortcut key is pressed + */ + keyActionPressed(position: number) { + if (this.interactive) { + if (position < 0) { + this.action_icons[this.action_icons.length - 1].processClick(); + } else if (position < this.action_icons.length - 1) { + this.action_icons[position].processClick(); + } + } + } + + /** + * Remove all the action icons + */ + clearAll(): void { + this.action_icons.forEach(action => action.destroy()); + this.action_icons = []; + } + + /** + * Add an action icon + */ + addAction(ship: Ship, action: BaseAction): ActionIcon { + var icon = new ActionIcon(this, ship, action, this.action_icons.length); + icon.moveTo(this.actions, 74 + this.action_icons.length * 138, 58); + this.action_icons.push(icon); + + return icon; + } + + /** + * Update the power indicator + */ + updatePower(move_power = 0, fire_power = 0): void { + let power_capacity = this.ship ? this.ship.getAttribute("power_capacity") : 0; + let power_value = this.ship ? this.ship.getValue("power") : 0; + + let current_power = this.power_icons.length; + + if (current_power > power_capacity) { + destroyChildren(this.power_icons, power_capacity, current_power); + } else if (power_capacity > current_power) { + range(power_capacity - current_power).forEach(i => { + let x = (current_power + i) % 5; + let y = ((current_power + i) - x) / 5; + let image = new UIBuilder(this.battleview, this.power_icons).image("battle-actionbar-power-used", x * 43, y * 22); + }); + } + + let remaining_power = power_value - move_power - fire_power; + this.power_icons.list.forEach((obj, idx) => { + let img = obj; + if (idx < remaining_power) { + this.battleview.changeImage(img, "battle-actionbar-power-available"); + } else if (idx < remaining_power + move_power) { + this.battleview.changeImage(img, "battle-actionbar-power-move"); + } else if (idx < power_value) { + this.battleview.changeImage(img, "battle-actionbar-power-fire"); + } else { + this.battleview.changeImage(img, "battle-actionbar-power-used"); + } + }); + } + + /** + * Temporarily set current action power usage. + * + * When an action is selected, this will fade the icons not available after the action would be done. + * This will also highlight power usage in the power bar. + * + * *move_power* and *fire_power* is the consumption of currently selected action/target. + */ + updateSelectedActionPower(move_power: number, fire_power: number, action: BaseAction): void { + this.action_icons.forEach(icon => icon.refresh(action, move_power + fire_power)); + this.updatePower(move_power, fire_power); + } + + /** + * Temporarily set power status for a given move-fire simulation + */ + updateFromSimulation(action: BaseAction, simulation: MoveFireResult) { + if (simulation.complete) { + this.updateSelectedActionPower(simulation.total_move_ap, simulation.total_fire_ap, action); + } else { + this.updateSelectedActionPower(0, 0, action); + } + } + + /** + * Set the bar to display a given ship + */ + setShip(ship: Ship | null): void { + this.clearAll(); + + if (ship && this.battleview.player.is(ship.fleet.player) && ship.alive) { + ship.actions.listAll().forEach(action => this.addAction(ship, action)); + this.ship = ship; + } else { + this.ship = null; + } + + this.updatePower(); + } + + // Called by an action icon when the action is selected + actionStarted(): void { + } + + // Called by an action icon when the action has been applied + actionEnded(): void { + this.battleview.exitTargettingMode(); + + this.updatePower(); + this.action_icons.forEach(action => action.refresh()); + } } diff --git a/src/ui/battle/ActionIcon.spec.ts b/src/ui/battle/ActionIcon.spec.ts index 9c9df85..db16dba 100644 --- a/src/ui/battle/ActionIcon.spec.ts +++ b/src/ui/battle/ActionIcon.spec.ts @@ -1,139 +1,146 @@ -module TK.SpaceTac.UI.Specs { - testing("ActionIcon", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { BaseAction } from "../../core/actions/BaseAction"; +import { ToggleAction } from "../../core/actions/ToggleAction"; +import { TriggerAction } from "../../core/actions/TriggerAction"; +import { Ship } from "../../core/Ship"; +import { TestTools } from "../../core/TestTools"; +import { setupBattleview } from "../TestGame"; +import { ActionIcon } from "./ActionIcon"; - test.case("displays power usage", check => { - let bar = testgame.view.action_bar; - let ship = new Ship(); - let action = new BaseAction("something"); - let icon = new ActionIcon(bar, ship, action, 0); - check.same(icon.power_container.visible, false, "initial state"); +testing("ActionIcon", test => { + let testgame = setupBattleview(test); - icon.refresh(); - check.same(icon.power_container.visible, false, "no change"); + test.case("displays power usage", check => { + let bar = testgame.view.action_bar; + let ship = new Ship(); + let action = new BaseAction("something"); + let icon = new ActionIcon(bar, ship, action, 0); + check.same(icon.power_container.visible, false, "initial state"); - let cost = 3; - check.patch(action, "getPowerUsage", () => cost); - icon.refresh(); - check.in("power cost = 3", check => { - check.equals(icon.power_container.visible, true); - check.equals(icon.power_text.text, "3\n-"); - }); - cost = -2; - icon.refresh(); - check.in("power cost = -2", check => { - check.equals(icon.power_container.visible, true); - check.equals(icon.power_text.text, "2\n+"); - }); - }) + icon.refresh(); + check.same(icon.power_container.visible, false, "no change"); - test.case("displays disabled and fading states", check => { - let bar = testgame.view.action_bar; - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, 5); - let action = TestTools.addWeapon(ship, 50, 3); - let icon = new ActionIcon(bar, ship, action, 0); - - check.equals(icon.container.name, "battle-actionbar-frame-enabled", "5/5"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "5/5"); - check.same(icon.power_bg.visible, true, "5/5"); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "5/5"); - - icon.refresh(null, 1); - check.equals(icon.container.name, "battle-actionbar-frame-enabled", "4/5"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "4/5"); - check.same(icon.power_bg.visible, true, "4/5"); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "4/5"); - - icon.refresh(null, 4); - check.equals(icon.container.name, "battle-actionbar-frame-fading", "1/5"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "1/5"); - check.same(icon.power_bg.visible, true, "1/5"); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "1/5"); - - ship.setValue("power", 2); - icon.refresh(); - check.equals(icon.container.name, "battle-actionbar-frame-disabled", "2/2"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-disabled", "2/2"); - check.same(icon.power_bg.visible, true, "2/2"); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-disabled", "2/2"); - - icon.refresh(null, 6); - check.equals(icon.container.name, "battle-actionbar-frame-disabled", "0/2"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-disabled", "0/2"); - check.same(icon.power_bg.visible, true, "0/2"); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-disabled", "0/2"); - }) - - test.case("displays toggle state", check => { - let bar = testgame.view.action_bar; - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, 5); - let action = new ToggleAction("toggle", { power: 2 }); - ship.actions.addCustom(action); - let icon = new ActionIcon(bar, ship, action, 0); - - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "initial"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "initial"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial"); - check.same(icon.img_cooldown.visible, true, "initial"); - - ship.actions.toggle(action, true); - icon.refresh(); - check.equals(icon.img_bottom.name, "battle-actionbar-bottom-toggled", "initial"); - check.equals(icon.power_bg.name, "battle-actionbar-consumption-toggled", "initial"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-toggled", "initial"); - check.same(icon.img_cooldown.visible, true, "initial"); - }) - - test.case("displays overheat/cooldown", check => { - let bar = testgame.view.action_bar; - let ship = new Ship(); - let action = new TriggerAction("weapon"); - - action.configureCooldown(1, 3); - TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); - let icon = new ActionIcon(bar, ship, action, 0); - check.same(icon.img_cooldown.visible, false, "initial"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial"); - check.same(icon.img_cooldown_group.length, 1, "initial"); - - icon.refresh(action); - check.same(icon.img_cooldown.visible, true, "overheat"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "overheat"); - check.same(icon.img_cooldown_group.length, 4, "overheat"); - - action.configureCooldown(1, 12); - TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); - icon.refresh(action); - check.same(icon.img_cooldown.visible, true, "superheat"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "superheat"); - check.same(icon.img_cooldown_group.length, 6, "superheat"); - - action.configureCooldown(1, 4); - TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); - ship.actions.getCooldown(action).use(); - icon.refresh(action); - check.same(icon.img_cooldown.visible, true, "cooling"); - check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-disabled", "cooling"); - check.same(icon.img_cooldown_group.length, 5, "cooling"); - }) - - test.case("displays currently targetting", check => { - testgame.view.animations.setImmediate(); - - let bar = testgame.view.action_bar; - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, 5); - let action = TestTools.addWeapon(ship, 50, 3); - let icon = new ActionIcon(bar, ship, action, 0); - check.same(icon.img_targetting.visible, false, "initial"); - - icon.refresh(action); - check.same(icon.img_targetting.visible, true, "selected"); - - icon.refresh(new BaseAction("other")); - check.same(icon.img_targetting.visible, false, "other"); - }) + let cost = 3; + check.patch(action, "getPowerUsage", () => cost); + icon.refresh(); + check.in("power cost = 3", check => { + check.equals(icon.power_container.visible, true); + check.equals(icon.power_text.text, "3\n-"); }); -} + cost = -2; + icon.refresh(); + check.in("power cost = -2", check => { + check.equals(icon.power_container.visible, true); + check.equals(icon.power_text.text, "2\n+"); + }); + }) + + test.case("displays disabled and fading states", check => { + let bar = testgame.view.action_bar; + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, 5); + let action = TestTools.addWeapon(ship, 50, 3); + let icon = new ActionIcon(bar, ship, action, 0); + + check.equals(icon.container.name, "battle-actionbar-frame-enabled", "5/5"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "5/5"); + check.same(icon.power_bg.visible, true, "5/5"); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "5/5"); + + icon.refresh(null, 1); + check.equals(icon.container.name, "battle-actionbar-frame-enabled", "4/5"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "4/5"); + check.same(icon.power_bg.visible, true, "4/5"); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "4/5"); + + icon.refresh(null, 4); + check.equals(icon.container.name, "battle-actionbar-frame-fading", "1/5"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "1/5"); + check.same(icon.power_bg.visible, true, "1/5"); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "1/5"); + + ship.setValue("power", 2); + icon.refresh(); + check.equals(icon.container.name, "battle-actionbar-frame-disabled", "2/2"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-disabled", "2/2"); + check.same(icon.power_bg.visible, true, "2/2"); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-disabled", "2/2"); + + icon.refresh(null, 6); + check.equals(icon.container.name, "battle-actionbar-frame-disabled", "0/2"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-disabled", "0/2"); + check.same(icon.power_bg.visible, true, "0/2"); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-disabled", "0/2"); + }) + + test.case("displays toggle state", check => { + let bar = testgame.view.action_bar; + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, 5); + let action = new ToggleAction("toggle", { power: 2 }); + ship.actions.addCustom(action); + let icon = new ActionIcon(bar, ship, action, 0); + + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "initial"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-enabled", "initial"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial"); + check.same(icon.img_cooldown.visible, true, "initial"); + + ship.actions.toggle(action, true); + icon.refresh(); + check.equals(icon.img_bottom.name, "battle-actionbar-bottom-toggled", "initial"); + check.equals(icon.power_bg.name, "battle-actionbar-consumption-toggled", "initial"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-toggled", "initial"); + check.same(icon.img_cooldown.visible, true, "initial"); + }) + + test.case("displays overheat/cooldown", check => { + let bar = testgame.view.action_bar; + let ship = new Ship(); + let action = new TriggerAction("weapon"); + + action.configureCooldown(1, 3); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); + let icon = new ActionIcon(bar, ship, action, 0); + check.same(icon.img_cooldown.visible, false, "initial"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial"); + check.same(icon.img_cooldown_group.length, 1, "initial"); + + icon.refresh(action); + check.same(icon.img_cooldown.visible, true, "overheat"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "overheat"); + check.same(icon.img_cooldown_group.length, 4, "overheat"); + + action.configureCooldown(1, 12); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); + icon.refresh(action); + check.same(icon.img_cooldown.visible, true, "superheat"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "superheat"); + check.same(icon.img_cooldown_group.length, 6, "superheat"); + + action.configureCooldown(1, 4); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); + ship.actions.getCooldown(action).use(); + icon.refresh(action); + check.same(icon.img_cooldown.visible, true, "cooling"); + check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-disabled", "cooling"); + check.same(icon.img_cooldown_group.length, 5, "cooling"); + }) + + test.case("displays currently targetting", check => { + testgame.view.animations.setImmediate(); + + let bar = testgame.view.action_bar; + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, 5); + let action = TestTools.addWeapon(ship, 50, 3); + let icon = new ActionIcon(bar, ship, action, 0); + check.same(icon.img_targetting.visible, false, "initial"); + + icon.refresh(action); + check.same(icon.img_targetting.visible, true, "selected"); + + icon.refresh(new BaseAction("other")); + check.same(icon.img_targetting.visible, false, "other"); + }) +}); diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index 4372520..a556912 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -1,267 +1,281 @@ -module TK.SpaceTac.UI { - /** - * Icon to activate a ship ability - */ - export class ActionIcon { - // Link to parents - bar: ActionBar - view: BattleView +import { bool, range } from "../../common/Tools" +import { ActionTargettingMode, BaseAction } from "../../core/actions/BaseAction" +import { EndTurnAction } from "../../core/actions/EndTurnAction" +import { ToggleAction } from "../../core/actions/ToggleAction" +import { Ship } from "../../core/Ship" +import { Target } from "../../core/Target" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { UIText } from "../common/UIText" +import { destroyChildren } from "../common/UITools" +import { ActionBar } from "./ActionBar" +import { ActionTooltip } from "./ActionTooltip" +import { BattleView } from "./BattleView" - // Container - container: UIButton +/** + * Icon to activate a ship ability + */ +export class ActionIcon { + // Link to parents + bar: ActionBar + view: BattleView - // Related ship - ship: Ship + // Container + container: UIButton - // Related game action - action: BaseAction + // Related ship + ship: Ship - // Current state - fading = false - disabled = true - selected = false - toggled = false - targetting = false - cooldown = 0 + // Related game action + action: BaseAction - // Images - img_targetting!: UIImage - img_bottom: UIImage - img_cooldown_group: UIContainer - img_cooldown: UIImage - img_action: UIImage + // Current state + fading = false + disabled = true + selected = false + toggled = false + targetting = false + cooldown = 0 - // Keyboard shortcut - shortcut_container?: UIContainer + // Images + img_targetting!: UIImage + img_bottom: UIImage + img_cooldown_group: UIContainer + img_cooldown: UIImage + img_action: UIImage - // Power usage indicator - power_container: UIContainer - power_bg: UIImage - power_text: UIText + // Keyboard shortcut + shortcut_container?: UIContainer - constructor(bar: ActionBar, ship: Ship, action: BaseAction, position: number) { - this.bar = bar; - this.view = bar.battleview; + // Power usage indicator + power_container: UIContainer + power_bg: UIImage + power_text: UIText - let builder = new UIBuilder(this.view); - this.container = builder.button("battle-actionbar-frame-disabled", 0, 0, () => this.processClick(), filler => { - ActionTooltip.fill(filler, this.ship, this.action, position); - return true; - }, undefined, { center: true, hover_bottom: true }); - builder = builder.in(this.container); + constructor(bar: ActionBar, ship: Ship, action: BaseAction, position: number) { + this.bar = bar; + this.view = bar.battleview; - this.ship = ship; - this.action = action; + let builder = new UIBuilder(this.view); + this.container = builder.button("battle-actionbar-frame-disabled", 0, 0, () => this.processClick(), filler => { + ActionTooltip.fill(filler, this.ship, this.action, position); + return true; + }, undefined, { center: true, hover_bottom: true }); + builder = builder.in(this.container); - // Action icon - this.img_action = builder.image(`action-${action.code}`, 0, 0, true); - this.img_action.setScale(0.35); - this.img_action.setAlpha(0.2); + this.ship = ship; + this.action = action; - // Hotkey indicator - if (!(action instanceof EndTurnAction)) { - this.shortcut_container = builder.container("shortcut", 0, -47); - builder.in(this.shortcut_container, builder => { - builder.image("battle-actionbar-hotkey", 0, 0, true); - builder.text(`${(position + 1) % 10}`, 0, -4, { - size: 12, color: "#d1d1d1", shadow: true, center: true, vcenter: true - }); - }); - } + // Action icon + this.img_action = builder.image(`action-${action.code}`, 0, 0, true); + this.img_action.setScale(0.35); + this.img_action.setAlpha(0.2); - // Bottom indicator - this.img_bottom = builder.image("battle-actionbar-bottom-disabled", 0, 40, true); - builder.in(this.img_bottom, builder => { - this.img_targetting = builder.image("battle-actionbar-bottom-targetting", 0, 12, true); - this.img_targetting.setVisible(false); - }); - - // Left indicator - this.selected = false; - this.power_container = builder.container("power", -46, -4, false); - this.power_bg = builder.in(this.power_container).image("battle-actionbar-consumption-disabled", 0, 0, true); - this.power_text = builder.in(this.power_container).text("", -2, 4, { - size: 16, color: "#ffdd4b", shadow: true, center: true, vcenter: true - }); - - // Right indicator - this.img_cooldown_group = builder.container("cooldown", 46, -4, action instanceof ToggleAction); - this.img_cooldown = builder.in(this.img_cooldown_group).image("battle-actionbar-sticky-untoggled", 0, 0, true); - - // Initialize - this.refresh(); - } - - /** - * Destroy the icon - */ - destroy(): void { - this.container.destroy(); - } - - /** - * Move to a given layer and position - */ - moveTo(layer: UIContainer, x = 0, y = 0): void { - layer.add(this.container); - this.container.setPosition(x, y); - } - - /** - * Process a click event on the action icon - * - * This will enter the action's targetting mode, waiting for a target or confirmation to apply the action - */ - processClick(): void { - if (!this.bar.interactive) { - return; - } - if (this.action.checkCannotBeApplied(this.ship)) { - return; - } - - this.view.audio.playOnce("ui-button-click"); - - if (this.selected) { - this.bar.actionEnded(); - return; - } - - // End any previously selected action - this.bar.actionEnded(); - this.bar.actionStarted(); - - let mode = this.action.getTargettingMode(this.ship); - if (mode == ActionTargettingMode.SELF || mode == ActionTargettingMode.SELF_CONFIRM) { - // Apply immediately on the ship - // TODO Handle confirm - this.processSelection(Target.newFromShip(this.ship)); - } else { - // Switch to targetting mode (will apply action when a target is selected) - this.view.enterTargettingMode(this.ship, this.action, mode); - } - } - - /** - * Called when a target is selected - * - * This will effectively apply the action - */ - processSelection(target: Target): void { - if (this.view.applyAction(this.action, target)) { - this.bar.actionEnded(); - } - } - - /** - * Update the display elements - * - * A currently targetting action may be passed, with power usage, to display potential fading and cooldown. - */ - refresh(used: BaseAction | null = null, power_consumption = 0): void { - let disabled = bool(this.action.checkCannotBeApplied(this.ship)); - let selected = (used === this.action); - let toggled = (this.action instanceof ToggleAction) && this.ship.actions.isToggled(this.action); - let fading = bool(this.action.checkCannotBeApplied(this.ship, this.ship.getValue("power") - power_consumption)); - let cooldown = this.ship.actions.getCooldown(this.action); - let heat = cooldown.heat; - let targetting = used !== null; - if (this.action == used && cooldown.willOverheat()) { - fading = true; - heat = cooldown.cooling; - } - - // inputs - if (disabled != this.disabled) { - //this.container.input.useHandCursor = !disabled; - } - - // frame - if (disabled != this.disabled || fading != this.fading) { - let name = "battle-actionbar-frame-enabled"; - if (disabled) { - name = "battle-actionbar-frame-disabled"; - } else if (fading) { - name = "battle-actionbar-frame-fading"; - } - this.container.setBaseImage(name); - } - - // action icon - if (disabled != this.disabled) { - this.img_action.alpha = disabled ? 0.2 : 1; - } - - // top - if (this.shortcut_container && (targetting != this.targetting || disabled != this.disabled)) { - this.view.animations.setVisible(this.shortcut_container, !targetting, 200, disabled ? 0.2 : 1); - } - - // bottom - if (disabled != this.disabled || toggled != this.toggled) { - if (disabled) { - this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-disabled"); - } else if (toggled) { - this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-toggled"); - } else { - this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-enabled"); - } - } - if (selected != this.selected) { - this.view.animations.setVisible(this.img_targetting, selected, 200); - } - - // left - let cost = this.action.getPowerUsage(this.ship, null); - this.power_container.setVisible(bool(cost)); - this.power_text.setText(`${Math.abs(cost)}\n${cost < 0 ? "+" : "-"}`); - this.power_text.setColor((cost > 0) ? "#ffdd4b" : "#dbe748"); - this.power_text.setAlpha(disabled ? 0.2 : 1); - if (disabled != this.disabled || selected != this.selected || toggled != this.toggled) { - if (disabled) { - this.view.changeImage(this.power_bg, "battle-actionbar-consumption-disabled"); - } else if (toggled) { - this.view.changeImage(this.power_bg, "battle-actionbar-consumption-toggled"); - } else if (selected) { - this.view.changeImage(this.power_bg, "battle-actionbar-consumption-targetting"); - } else { - this.view.changeImage(this.power_bg, "battle-actionbar-consumption-enabled"); - } - } - - // right - if (toggled != this.toggled || disabled != this.disabled || heat != this.cooldown) { - let builder = new UIBuilder(this.view, this.img_cooldown_group); - destroyChildren(this.img_cooldown_group, 1); - if (this.action instanceof ToggleAction) { - if (toggled) { - builder.change(this.img_cooldown, "battle-actionbar-sticky-toggled"); - } else { - builder.change(this.img_cooldown, "battle-actionbar-sticky-untoggled"); - } - this.img_cooldown.visible = !disabled; - } else if (heat) { - if (disabled) { - builder.change(this.img_cooldown, "battle-actionbar-sticky-disabled"); - } else { - builder.change(this.img_cooldown, "battle-actionbar-sticky-overheat"); - } - range(Math.min(heat - 1, 4)).forEach(i => { - builder.image("battle-actionbar-cooldown-one", 0, 2 - i * 7); - }); - builder.image("battle-actionbar-cooldown-front", -4, -20); - this.img_cooldown.visible = true; - } else { - this.img_cooldown.visible = false; - } - } - - this.disabled = disabled; - this.selected = selected; - this.targetting = targetting; - this.fading = fading; - this.toggled = toggled; - this.cooldown = heat; - } + // Hotkey indicator + if (!(action instanceof EndTurnAction)) { + this.shortcut_container = builder.container("shortcut", 0, -47); + builder.in(this.shortcut_container, builder => { + builder.image("battle-actionbar-hotkey", 0, 0, true); + builder.text(`${(position + 1) % 10}`, 0, -4, { + size: 12, color: "#d1d1d1", shadow: true, center: true, vcenter: true + }); + }); } + + // Bottom indicator + this.img_bottom = builder.image("battle-actionbar-bottom-disabled", 0, 40, true); + builder.in(this.img_bottom, builder => { + this.img_targetting = builder.image("battle-actionbar-bottom-targetting", 0, 12, true); + this.img_targetting.setVisible(false); + }); + + // Left indicator + this.selected = false; + this.power_container = builder.container("power", -46, -4, false); + this.power_bg = builder.in(this.power_container).image("battle-actionbar-consumption-disabled", 0, 0, true); + this.power_text = builder.in(this.power_container).text("", -2, 4, { + size: 16, color: "#ffdd4b", shadow: true, center: true, vcenter: true + }); + + // Right indicator + this.img_cooldown_group = builder.container("cooldown", 46, -4, action instanceof ToggleAction); + this.img_cooldown = builder.in(this.img_cooldown_group).image("battle-actionbar-sticky-untoggled", 0, 0, true); + + // Initialize + this.refresh(); + } + + /** + * Destroy the icon + */ + destroy(): void { + this.container.destroy(); + } + + /** + * Move to a given layer and position + */ + moveTo(layer: UIContainer, x = 0, y = 0): void { + layer.add(this.container); + this.container.setPosition(x, y); + } + + /** + * Process a click event on the action icon + * + * This will enter the action's targetting mode, waiting for a target or confirmation to apply the action + */ + processClick(): void { + if (!this.bar.interactive) { + return; + } + if (this.action.checkCannotBeApplied(this.ship)) { + return; + } + + this.view.audio.playOnce("ui-button-click"); + + if (this.selected) { + this.bar.actionEnded(); + return; + } + + // End any previously selected action + this.bar.actionEnded(); + this.bar.actionStarted(); + + let mode = this.action.getTargettingMode(this.ship); + if (mode == ActionTargettingMode.SELF || mode == ActionTargettingMode.SELF_CONFIRM) { + // Apply immediately on the ship + // TODO Handle confirm + this.processSelection(Target.newFromShip(this.ship)); + } else { + // Switch to targetting mode (will apply action when a target is selected) + this.view.enterTargettingMode(this.ship, this.action, mode); + } + } + + /** + * Called when a target is selected + * + * This will effectively apply the action + */ + processSelection(target: Target): void { + if (this.view.applyAction(this.action, target)) { + this.bar.actionEnded(); + } + } + + /** + * Update the display elements + * + * A currently targetting action may be passed, with power usage, to display potential fading and cooldown. + */ + refresh(used: BaseAction | null = null, power_consumption = 0): void { + let disabled = bool(this.action.checkCannotBeApplied(this.ship)); + let selected = (used === this.action); + let toggled = (this.action instanceof ToggleAction) && this.ship.actions.isToggled(this.action); + let fading = bool(this.action.checkCannotBeApplied(this.ship, this.ship.getValue("power") - power_consumption)); + let cooldown = this.ship.actions.getCooldown(this.action); + let heat = cooldown.heat; + let targetting = used !== null; + if (this.action == used && cooldown.willOverheat()) { + fading = true; + heat = cooldown.cooling; + } + + // inputs + if (disabled != this.disabled) { + //this.container.input.useHandCursor = !disabled; + } + + // frame + if (disabled != this.disabled || fading != this.fading) { + let name = "battle-actionbar-frame-enabled"; + if (disabled) { + name = "battle-actionbar-frame-disabled"; + } else if (fading) { + name = "battle-actionbar-frame-fading"; + } + this.container.setBaseImage(name); + } + + // action icon + if (disabled != this.disabled) { + this.img_action.alpha = disabled ? 0.2 : 1; + } + + // top + if (this.shortcut_container && (targetting != this.targetting || disabled != this.disabled)) { + this.view.animations.setVisible(this.shortcut_container, !targetting, 200, disabled ? 0.2 : 1); + } + + // bottom + if (disabled != this.disabled || toggled != this.toggled) { + if (disabled) { + this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-disabled"); + } else if (toggled) { + this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-toggled"); + } else { + this.view.changeImage(this.img_bottom, "battle-actionbar-bottom-enabled"); + } + } + if (selected != this.selected) { + this.view.animations.setVisible(this.img_targetting, selected, 200); + } + + // left + let cost = this.action.getPowerUsage(this.ship, null); + this.power_container.setVisible(bool(cost)); + this.power_text.setText(`${Math.abs(cost)}\n${cost < 0 ? "+" : "-"}`); + this.power_text.setColor((cost > 0) ? "#ffdd4b" : "#dbe748"); + this.power_text.setAlpha(disabled ? 0.2 : 1); + if (disabled != this.disabled || selected != this.selected || toggled != this.toggled) { + if (disabled) { + this.view.changeImage(this.power_bg, "battle-actionbar-consumption-disabled"); + } else if (toggled) { + this.view.changeImage(this.power_bg, "battle-actionbar-consumption-toggled"); + } else if (selected) { + this.view.changeImage(this.power_bg, "battle-actionbar-consumption-targetting"); + } else { + this.view.changeImage(this.power_bg, "battle-actionbar-consumption-enabled"); + } + } + + // right + if (toggled != this.toggled || disabled != this.disabled || heat != this.cooldown) { + let builder = new UIBuilder(this.view, this.img_cooldown_group); + destroyChildren(this.img_cooldown_group, 1); + if (this.action instanceof ToggleAction) { + if (toggled) { + builder.change(this.img_cooldown, "battle-actionbar-sticky-toggled"); + } else { + builder.change(this.img_cooldown, "battle-actionbar-sticky-untoggled"); + } + this.img_cooldown.visible = !disabled; + } else if (heat) { + if (disabled) { + builder.change(this.img_cooldown, "battle-actionbar-sticky-disabled"); + } else { + builder.change(this.img_cooldown, "battle-actionbar-sticky-overheat"); + } + range(Math.min(heat - 1, 4)).forEach(i => { + builder.image("battle-actionbar-cooldown-one", 0, 2 - i * 7); + }); + builder.image("battle-actionbar-cooldown-front", -4, -20); + this.img_cooldown.visible = true; + } else { + this.img_cooldown.visible = false; + } + } + + this.disabled = disabled; + this.selected = selected; + this.targetting = targetting; + this.fading = fading; + this.toggled = toggled; + this.cooldown = heat; + } } diff --git a/src/ui/battle/ActionTooltip.spec.ts b/src/ui/battle/ActionTooltip.spec.ts index 1832546..4f4dd00 100644 --- a/src/ui/battle/ActionTooltip.spec.ts +++ b/src/ui/battle/ActionTooltip.spec.ts @@ -1,34 +1,43 @@ -module TK.SpaceTac.UI.Specs { - testing("ActionTooltip", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { EndTurnAction } from "../../core/actions/EndTurnAction"; +import { MoveAction } from "../../core/actions/MoveAction"; +import { TriggerAction } from "../../core/actions/TriggerAction"; +import { DamageEffect } from "../../core/effects/DamageEffect"; +import { Ship } from "../../core/Ship"; +import { TestTools } from "../../core/TestTools"; +import { Tooltip } from "../common/Tooltip"; +import { checkText, setupEmptyView } from "../TestGame"; +import { ActionTooltip } from "./ActionTooltip"; - test.case("displays action information", check => { - let tooltip = new Tooltip(testgame.view); - let ship = new Ship(); - TestTools.setShipModel(ship, 100, 0, 10); +testing("ActionTooltip", test => { + let testgame = setupEmptyView(test); - let action1 = ship.actions.addCustom(new MoveAction("Thruster")); - let action2 = ship.actions.addCustom(new TriggerAction("Superweapon", { effects: [new DamageEffect(12)], power: 2, range: 50 })); - let action3 = ship.actions.addCustom(new EndTurnAction()); + test.case("displays action information", check => { + let tooltip = new Tooltip(testgame.view); + let ship = new Ship(); + TestTools.setShipModel(ship, 100, 0, 10); - ActionTooltip.fill(tooltip.getBuilder(), ship, action1, 0); - checkText(check, tooltip.container.content.list[1], "Use Thruster"); - checkText(check, tooltip.container.content.list[2], "Cost: 1 power per 0km"); - checkText(check, tooltip.container.content.list[3], "Move: 0km per power point (safety: 120km)"); - checkText(check, tooltip.container.content.list[4], "[ 1 ]"); + let action1 = ship.actions.addCustom(new MoveAction("Thruster")); + let action2 = ship.actions.addCustom(new TriggerAction("Superweapon", { effects: [new DamageEffect(12)], power: 2, range: 50 })); + let action3 = ship.actions.addCustom(new EndTurnAction()); - tooltip.hide(); - ActionTooltip.fill(tooltip.getBuilder(), ship, action2, 1); - checkText(check, tooltip.container.content.list[1], "Fire Superweapon"); - checkText(check, tooltip.container.content.list[2], "Cost: 2 power"); - checkText(check, tooltip.container.content.list[3], "Fire (power 2, range 50km):\n• do 12 damage on target"); - checkText(check, tooltip.container.content.list[4], "[ 2 ]"); + ActionTooltip.fill(tooltip.getBuilder(), ship, action1, 0); + checkText(check, tooltip.container.content.list[1], "Use Thruster"); + checkText(check, tooltip.container.content.list[2], "Cost: 1 power per 0km"); + checkText(check, tooltip.container.content.list[3], "Move: 0km per power point (safety: 120km)"); + checkText(check, tooltip.container.content.list[4], "[ 1 ]"); - tooltip.hide(); - ActionTooltip.fill(tooltip.getBuilder(), ship, action3, 2); - checkText(check, tooltip.container.content.list[1], "End turn"); - checkText(check, tooltip.container.content.list[2], "End the current ship's turn.\nWill also generate power and cool down equipments."); - checkText(check, tooltip.container.content.list[3], "[ space ]"); - }); - }); -} + tooltip.hide(); + ActionTooltip.fill(tooltip.getBuilder(), ship, action2, 1); + checkText(check, tooltip.container.content.list[1], "Fire Superweapon"); + checkText(check, tooltip.container.content.list[2], "Cost: 2 power"); + checkText(check, tooltip.container.content.list[3], "Fire (power 2, range 50km):\n• do 12 damage on target"); + checkText(check, tooltip.container.content.list[4], "[ 2 ]"); + + tooltip.hide(); + ActionTooltip.fill(tooltip.getBuilder(), ship, action3, 2); + checkText(check, tooltip.container.content.list[1], "End turn"); + checkText(check, tooltip.container.content.list[2], "End the current ship's turn.\nWill also generate power and cool down equipments."); + checkText(check, tooltip.container.content.list[3], "[ space ]"); + }); +}); diff --git a/src/ui/battle/ActionTooltip.ts b/src/ui/battle/ActionTooltip.ts index fce461e..ff48eea 100644 --- a/src/ui/battle/ActionTooltip.ts +++ b/src/ui/battle/ActionTooltip.ts @@ -1,69 +1,74 @@ -module TK.SpaceTac.UI { - /** - * Tooltip displaying action information - */ - export class ActionTooltip { - /** - * Fill the tooltip - */ - static fill(filler: TooltipBuilder, ship: Ship, action: BaseAction, position?: number): boolean { - let builder = filler.styled({ size: 20 }); +import { BaseAction } from "../../core/actions/BaseAction"; +import { EndTurnAction } from "../../core/actions/EndTurnAction"; +import { MoveAction } from "../../core/actions/MoveAction"; +import { ToggleAction } from "../../core/actions/ToggleAction"; +import { Ship } from "../../core/Ship"; +import { TooltipBuilder } from "../common/Tooltip"; - let icon = builder.image(`action-${action.code}`); - icon.setScale(0.5); +/** + * Tooltip displaying action information + */ +export class ActionTooltip { + /** + * Fill the tooltip + */ + static fill(filler: TooltipBuilder, ship: Ship, action: BaseAction, position?: number): boolean { + let builder = filler.styled({ size: 20 }); - builder.text(action.getTitle(ship), 150, 0, { size: 24 }); + let icon = builder.image(`action-${action.code}`); + icon.setScale(0.5); - let unavailable = action.checkCannotBeApplied(ship); - if (unavailable != null) { - builder.text(unavailable, 150, 40, { color: "#e54d2b" }); - } else if (action instanceof MoveAction) { - let cost = `Cost: 1 power per ${action.distance_per_power}km`; - builder.text(cost, 150, 40, { color: "#ffdd4b" }); - } else { - let power_usage = action.getPowerUsage(ship, null); - if (power_usage) { - let cost = (power_usage > 0) ? `Cost: ${power_usage} power` : `Recover: ${-power_usage} power`; - builder.text(cost, 150, 40, { color: "#ffdd4b" }); - } - } + builder.text(action.getTitle(ship), 150, 0, { size: 24 }); - let cooldown = ship.actions.getCooldown(action); - if (cooldown.overheat) { - if (cooldown.heat > 0) { - builder.text("Cooling down ...", 150, 80, { color: "#d8894d" }); - } else if (!unavailable && cooldown.willOverheat()) { - if (cooldown.cooling > 1) { - let turns = cooldown.cooling - 1; - builder.text(`Unavailable for ${turns} turn${turns > 1 ? "s" : ""} if used`, 150, 80, { color: "#d8894d" }); - } else { - builder.text("Unavailable until next turn if used", 150, 80, { color: "#d8894d" }); - } - } - } else if (action instanceof ToggleAction && ship.actions.isToggled(action)) { - builder.text(`Activated`, 150, 80, { color: "#dbe748" }); - } - - let description = action.getEffectsDescription(); - if (description) { - builder.text(description, 30, 170, { size: 16 }); - } - - if (typeof position != "undefined") { - let shortcut = ""; - if (action instanceof EndTurnAction) { - shortcut = "[ space ]"; - } else if (position == 9) { - shortcut = "[ 0 ]"; - } else if (position >= 0 && position < 9) { - shortcut = `[ ${position + 1} ]`; - } - if (shortcut) { - builder.text(shortcut, 150, 120, { color: "#aaaaaa", size: 12 }); - } - } - - return true; - } + let unavailable = action.checkCannotBeApplied(ship); + if (unavailable != null) { + builder.text(unavailable, 150, 40, { color: "#e54d2b" }); + } else if (action instanceof MoveAction) { + let cost = `Cost: 1 power per ${action.distance_per_power}km`; + builder.text(cost, 150, 40, { color: "#ffdd4b" }); + } else { + let power_usage = action.getPowerUsage(ship, null); + if (power_usage) { + let cost = (power_usage > 0) ? `Cost: ${power_usage} power` : `Recover: ${-power_usage} power`; + builder.text(cost, 150, 40, { color: "#ffdd4b" }); + } } + + let cooldown = ship.actions.getCooldown(action); + if (cooldown.overheat) { + if (cooldown.heat > 0) { + builder.text("Cooling down ...", 150, 80, { color: "#d8894d" }); + } else if (!unavailable && cooldown.willOverheat()) { + if (cooldown.cooling > 1) { + let turns = cooldown.cooling - 1; + builder.text(`Unavailable for ${turns} turn${turns > 1 ? "s" : ""} if used`, 150, 80, { color: "#d8894d" }); + } else { + builder.text("Unavailable until next turn if used", 150, 80, { color: "#d8894d" }); + } + } + } else if (action instanceof ToggleAction && ship.actions.isToggled(action)) { + builder.text(`Activated`, 150, 80, { color: "#dbe748" }); + } + + let description = action.getEffectsDescription(); + if (description) { + builder.text(description, 30, 170, { size: 16 }); + } + + if (typeof position != "undefined") { + let shortcut = ""; + if (action instanceof EndTurnAction) { + shortcut = "[ space ]"; + } else if (position == 9) { + shortcut = "[ 0 ]"; + } else if (position >= 0 && position < 9) { + shortcut = `[ ${position + 1} ]`; + } + if (shortcut) { + builder.text(shortcut, 150, 120, { color: "#aaaaaa", size: 12 }); + } + } + + return true; + } } diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index f2d9a1a..723469d 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -1,327 +1,345 @@ -module TK.SpaceTac.UI { - /** - * Graphical representation of a battle - * - * This is the area in the BattleView that will display ships with their real positions - */ - export class Arena { - // Link to battleview - view: BattleView +import { iforeach } from "../../common/Iterators" +import { RObjectId } from "../../common/RObject" +import { first, minBy, remove } from "../../common/Tools" +import { arenaDistance, ArenaLocation } from "../../core/ArenaLocation" +import { Battle } from "../../core/Battle" +import { BaseBattleDiff } from "../../core/diffs/BaseBattleDiff" +import { DroneDeployedDiff, DroneRecalledDiff } from "../../core/diffs/DroneDeployedDiff" +import { Drone } from "../../core/Drone" +import { Ship } from "../../core/Ship" +import { MainUI } from "../../MainUI" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { IBounded } from "../common/UITools" +import { ArenaDrone } from "./ArenaDrone" +import { ArenaShip } from "./ArenaShip" +import { BattleView } from "./BattleView" +import { LogProcessorDelegate } from "./LogProcessor" +import { RangeHint } from "./RangeHint" - // Boundaries of the arena - private boundaries: IBounded = { x: 0, y: 0, width: 1808, height: 948 } +/** + * Graphical representation of a battle + * + * This is the area in the BattleView that will display ships with their real positions + */ +export class Arena { + // Link to battleview + view: BattleView - // Hint for weapon or move range - range_hint: RangeHint + // Boundaries of the arena + private boundaries: IBounded = { x: 0, y: 0, width: 1808, height: 948 } - // Input capture - private mouse_capture?: UIImage + // Hint for weapon or move range + range_hint: RangeHint - // List of ship sprites - private ship_sprites: ArenaShip[] = [] + // Input capture + private mouse_capture?: UIImage - // List of drone sprites - private drone_sprites: ArenaDrone[] = [] + // List of ship sprites + private ship_sprites: ArenaShip[] = [] - // Currently hovered ship - private hovered: ArenaShip | null - // Currently playing ship - private playing: ArenaShip | null + // List of drone sprites + private drone_sprites: ArenaDrone[] = [] - // Layer for particles - container: UIContainer - layer_garbage: UIContainer - layer_hints: UIContainer - layer_drones: UIContainer - layer_ships: UIContainer - layer_weapon_effects: UIContainer - layer_targetting: UIContainer + // Currently hovered ship + private hovered: ArenaShip | null + // Currently playing ship + private playing: ArenaShip | null - // Callbacks to receive cursor events - callbacks_hover: ((location: ArenaLocation | null, ship: Ship | null) => void)[] = [] - callbacks_click: (() => void)[] = [] + // Layer for particles + container: UIContainer + layer_garbage: UIContainer + layer_hints: UIContainer + layer_drones: UIContainer + layer_ships: UIContainer + layer_weapon_effects: UIContainer + layer_targetting: UIContainer - // Create a graphical arena for ship sprites to fight in a 2D space - constructor(view: BattleView, container?: UIContainer) { - this.view = view; - this.playing = null; - this.hovered = null; - this.range_hint = new RangeHint(this); + // Callbacks to receive cursor events + callbacks_hover: ((location: ArenaLocation | null, ship: Ship | null) => void)[] = [] + callbacks_click: (() => void)[] = [] - let builder = new UIBuilder(view, container); - if (!container) { - container = builder.container("arena"); - builder = builder.in(container); - } - this.container = container; - container.setPosition(this.boundaries.x, this.boundaries.y); + // Create a graphical arena for ship sprites to fight in a 2D space + constructor(view: BattleView, container?: UIContainer) { + this.view = view; + this.playing = null; + this.hovered = null; + this.range_hint = new RangeHint(this); - this.setupMouseCapture(); - - this.layer_garbage = builder.container("garbage"); - this.layer_hints = builder.container("hints"); - this.layer_drones = builder.container("drones"); - this.layer_ships = builder.container("ships"); - this.layer_weapon_effects = builder.container("effects"); - this.layer_targetting = builder.container("targetting"); - - this.range_hint.setLayer(this.layer_hints); - this.addShipSprites(); - view.battle.drones.list().forEach(drone => this.addDrone(drone, 0)); - - view.log_processor.register(diff => this.checkDroneDeployed(diff)); - view.log_processor.register(diff => this.checkDroneRecalled(diff)); - view.log_processor.watchForShipChange(ship => { - return { - foreground: async () => { - await this.setShipPlaying(ship) - } - } - }); - } - - /** - * Move to a specific layer - */ - moveToLayer(layer: UIContainer): void { - layer.add(this.container); - } - - /** - * Setup the mouse capture for targetting events - */ - setupMouseCapture() { - let view = this.view; - let button_down = false; - - let background = new UIBuilder(view, this.container).image("battle-arena-background"); - background.setName("mouse-capture"); - background.setScale(this.boundaries.width / background.width, this.boundaries.height / background.height) - - // Capture clicks on background - background.setInteractive(); - background.on("pointerdown", (pointer: Phaser.Input.Pointer) => { - button_down = (pointer.buttons == 1); - }); - background.on("pointerup", (pointer: Phaser.Input.Pointer) => { - if (button_down) { - button_down = false; - this.callbacks_click.forEach(callback => callback()); - } - }); - background.on("pointerout", () => { - this.callbacks_hover.forEach(callback => callback(null, null)); - }); - - // Watch mouse move to capture hovering over background - background.on("pointermove", (pointer: Phaser.Input.Pointer) => { - let location = new ArenaLocation(pointer.x / this.view.getScaling(), pointer.y / this.view.getScaling()); - let ship = this.getShip(location); - this.callbacks_hover.forEach(callback => callback(location, ship)); - }, null); - - this.mouse_capture = background; - } - - /** - * Get the ship under a cursor location - */ - getShip(location: ArenaLocation): Ship | null { - let nearest = minBy(this.ship_sprites, sprite => arenaDistance(location, sprite.ship.location)); - if (nearest && arenaDistance(location, nearest) < 50) { - return nearest.ship; - } else { - return null; - } - } - - /** - * Add the sprites for all ships - */ - addShipSprites() { - iforeach(this.view.battle.iships(), ship => { - let sprite = new ArenaShip(this, ship); - (ship.alive ? this.layer_ships : this.layer_garbage).add(sprite); - this.ship_sprites.push(sprite); - }); - } - - /** - * Get the current MainUI instance - */ - get game(): MainUI { - return this.view.gameui; - } - - /** - * Get the current battle displayed - */ - getBattle(): Battle { - return this.view.battle; - } - - // Remove a ship sprite - markAsDead(ship: Ship): void { - var sprite = this.findShipSprite(ship); - if (sprite) { - sprite.setDead(true); - this.layer_garbage.add(sprite); - } - } - - // Find the sprite for a ship - findShipSprite(ship: Ship | RObjectId | null): ArenaShip | null { - return first(this.ship_sprites, sprite => sprite.ship.is(ship)); - } - - // Set the hovered state on a ship sprite - setShipHovered(ship: Ship | null): void { - if (this.hovered) { - this.hovered.setHovered(false, false); - } - - if (ship) { - var arena_ship = this.findShipSprite(ship); - if (arena_ship) { - arena_ship.setHovered(true, false); - this.layer_ships.bringToTop(arena_ship); - } - this.hovered = arena_ship; - } else { - this.hovered = null; - } - } - - /** - * Set the playing state on a ship sprite - */ - async setShipPlaying(ship: Ship | null, animate = true): Promise { - if (this.playing) { - this.playing.setPlaying(false); - this.playing = null; - } - - if (ship) { - let arena_ship = this.findShipSprite(ship); - if (arena_ship) { - this.layer_ships.bringToTop(arena_ship); - await arena_ship.setPlaying(true, animate); - } - - this.playing = arena_ship; - } - } - - /** - * Find an ArenaDrone displaying a Drone. - */ - findDrone(drone: Drone | RObjectId | null): ArenaDrone | null { - return first(this.drone_sprites, sprite => sprite.drone.is(drone)); - } - - /** - * Spawn a new drone - */ - async addDrone(drone: Drone, speed = 1): Promise { - if (!this.findDrone(drone)) { - let sprite = new ArenaDrone(this.view, drone); - let owner = this.view.battle.getShip(drone.owner) || new Ship(); - let angle = Math.atan2(drone.y - owner.arena_y, drone.x - owner.arena_x); - this.layer_drones.add(sprite); - this.drone_sprites.push(sprite); - - if (speed) { - sprite.radius.setAlpha(0); - sprite.setPosition(owner.arena_x, owner.arena_y); - sprite.sprite.setRotation(owner.arena_angle); - await this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite, speed); - await this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500 / speed, "Cubic.easeIn"); - } else { - sprite.setPosition(drone.x, drone.y); - sprite.setRotation(angle); - } - - } else { - console.error("Drone added twice to arena", drone); - } - } - - /** - * Remove a destroyed drone - */ - async removeDrone(drone: Drone, speed = 1): Promise { - let sprite = this.findDrone(drone); - if (sprite) { - remove(this.drone_sprites, sprite); - return sprite.setDestroyed(speed); - } else { - console.error("Drone not found in arena for removal", drone); - } - } - - /** - * Switch the tactical mode (shows information on all ships, and fades background) - */ - setTacticalMode(active: boolean): void { - this.ship_sprites.forEach(sprite => sprite.setHovered(active, true)); - this.drone_sprites.forEach(drone => drone.setTacticalMode(active)); - this.view.animations.setVisible(this.layer_garbage, !active, 200); - if (this.view.background) { - this.view.animations.setVisible(this.view.background, !active, 200); - } - } - - /** - * Get the boundaries of the arena on display - */ - getBoundaries(scaled = false): IBounded { - if (scaled) { - let scaling = this.view.getScaling(); - return { - x: Math.ceil(this.boundaries.x * scaling), - y: Math.ceil(this.boundaries.y * scaling), - width: Math.floor(this.boundaries.width * scaling), - height: Math.floor(this.boundaries.height * scaling), - } - } else { - return this.boundaries; - } - } - - /** - * Check if a new drone as been deployed - */ - private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof DroneDeployedDiff) { - return { - foreground: async (speed: number) => { - if (speed) { - this.view.audio.playOnce("battle-drone-deploy"); - } - await this.addDrone(diff.drone, speed); - } - } - } else { - return {}; - } - } - - /** - * Check if a drone as been recalled - */ - private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof DroneRecalledDiff) { - return { - foreground: async (speed: number) => { - if (speed) { - this.view.audio.playOnce("battle-drone-destroy"); - } - await this.removeDrone(diff.drone, speed); - } - } - } else { - return {}; - } - } + let builder = new UIBuilder(view, container); + if (!container) { + container = builder.container("arena"); + builder = builder.in(container); } + this.container = container; + container.setPosition(this.boundaries.x, this.boundaries.y); + + this.setupMouseCapture(); + + this.layer_garbage = builder.container("garbage"); + this.layer_hints = builder.container("hints"); + this.layer_drones = builder.container("drones"); + this.layer_ships = builder.container("ships"); + this.layer_weapon_effects = builder.container("effects"); + this.layer_targetting = builder.container("targetting"); + + this.range_hint.setLayer(this.layer_hints); + this.addShipSprites(); + view.battle.drones.list().forEach(drone => this.addDrone(drone, 0)); + + view.log_processor.register(diff => this.checkDroneDeployed(diff)); + view.log_processor.register(diff => this.checkDroneRecalled(diff)); + view.log_processor.watchForShipChange(ship => { + return { + foreground: async () => { + await this.setShipPlaying(ship) + } + } + }); + } + + /** + * Move to a specific layer + */ + moveToLayer(layer: UIContainer): void { + layer.add(this.container); + } + + /** + * Setup the mouse capture for targetting events + */ + setupMouseCapture() { + let view = this.view; + let button_down = false; + + let background = new UIBuilder(view, this.container).image("battle-arena-background"); + background.setName("mouse-capture"); + background.setScale(this.boundaries.width / background.width, this.boundaries.height / background.height) + + // Capture clicks on background + background.setInteractive(); + background.on("pointerdown", (pointer: Phaser.Input.Pointer) => { + button_down = (pointer.buttons == 1); + }); + background.on("pointerup", (pointer: Phaser.Input.Pointer) => { + if (button_down) { + button_down = false; + this.callbacks_click.forEach(callback => callback()); + } + }); + background.on("pointerout", () => { + this.callbacks_hover.forEach(callback => callback(null, null)); + }); + + // Watch mouse move to capture hovering over background + background.on("pointermove", (pointer: Phaser.Input.Pointer) => { + let location = new ArenaLocation(pointer.x / this.view.getScaling(), pointer.y / this.view.getScaling()); + let ship = this.getShip(location); + this.callbacks_hover.forEach(callback => callback(location, ship)); + }, null); + + this.mouse_capture = background; + } + + /** + * Get the ship under a cursor location + */ + getShip(location: ArenaLocation): Ship | null { + let nearest = minBy(this.ship_sprites, sprite => arenaDistance(location, sprite.ship.location)); + if (nearest && arenaDistance(location, nearest) < 50) { + return nearest.ship; + } else { + return null; + } + } + + /** + * Add the sprites for all ships + */ + addShipSprites() { + iforeach(this.view.battle.iships(), ship => { + let sprite = new ArenaShip(this, ship); + (ship.alive ? this.layer_ships : this.layer_garbage).add(sprite); + this.ship_sprites.push(sprite); + }); + } + + /** + * Get the current MainUI instance + */ + get game(): MainUI { + return this.view.gameui; + } + + /** + * Get the current battle displayed + */ + getBattle(): Battle { + return this.view.battle; + } + + // Remove a ship sprite + markAsDead(ship: Ship): void { + var sprite = this.findShipSprite(ship); + if (sprite) { + sprite.setDead(true); + this.layer_garbage.add(sprite); + } + } + + // Find the sprite for a ship + findShipSprite(ship: Ship | RObjectId | null): ArenaShip | null { + return first(this.ship_sprites, sprite => sprite.ship.is(ship)); + } + + // Set the hovered state on a ship sprite + setShipHovered(ship: Ship | null): void { + if (this.hovered) { + this.hovered.setHovered(false, false); + } + + if (ship) { + var arena_ship = this.findShipSprite(ship); + if (arena_ship) { + arena_ship.setHovered(true, false); + this.layer_ships.bringToTop(arena_ship); + } + this.hovered = arena_ship; + } else { + this.hovered = null; + } + } + + /** + * Set the playing state on a ship sprite + */ + async setShipPlaying(ship: Ship | null, animate = true): Promise { + if (this.playing) { + this.playing.setPlaying(false); + this.playing = null; + } + + if (ship) { + let arena_ship = this.findShipSprite(ship); + if (arena_ship) { + this.layer_ships.bringToTop(arena_ship); + await arena_ship.setPlaying(true, animate); + } + + this.playing = arena_ship; + } + } + + /** + * Find an ArenaDrone displaying a Drone. + */ + findDrone(drone: Drone | RObjectId | null): ArenaDrone | null { + return first(this.drone_sprites, sprite => sprite.drone.is(drone)); + } + + /** + * Spawn a new drone + */ + async addDrone(drone: Drone, speed = 1): Promise { + if (!this.findDrone(drone)) { + let sprite = new ArenaDrone(this.view, drone); + let owner = this.view.battle.getShip(drone.owner) || new Ship(); + let angle = Math.atan2(drone.y - owner.arena_y, drone.x - owner.arena_x); + this.layer_drones.add(sprite); + this.drone_sprites.push(sprite); + + if (speed) { + sprite.radius.setAlpha(0); + sprite.setPosition(owner.arena_x, owner.arena_y); + sprite.sprite.setRotation(owner.arena_angle); + await this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite, speed); + await this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500 / speed, "Cubic.easeIn"); + } else { + sprite.setPosition(drone.x, drone.y); + sprite.setRotation(angle); + } + + } else { + console.error("Drone added twice to arena", drone); + } + } + + /** + * Remove a destroyed drone + */ + async removeDrone(drone: Drone, speed = 1): Promise { + let sprite = this.findDrone(drone); + if (sprite) { + remove(this.drone_sprites, sprite); + return sprite.setDestroyed(speed); + } else { + console.error("Drone not found in arena for removal", drone); + } + } + + /** + * Switch the tactical mode (shows information on all ships, and fades background) + */ + setTacticalMode(active: boolean): void { + this.ship_sprites.forEach(sprite => sprite.setHovered(active, true)); + this.drone_sprites.forEach(drone => drone.setTacticalMode(active)); + this.view.animations.setVisible(this.layer_garbage, !active, 200); + if (this.view.background) { + this.view.animations.setVisible(this.view.background, !active, 200); + } + } + + /** + * Get the boundaries of the arena on display + */ + getBoundaries(scaled = false): IBounded { + if (scaled) { + let scaling = this.view.getScaling(); + return { + x: Math.ceil(this.boundaries.x * scaling), + y: Math.ceil(this.boundaries.y * scaling), + width: Math.floor(this.boundaries.width * scaling), + height: Math.floor(this.boundaries.height * scaling), + } + } else { + return this.boundaries; + } + } + + /** + * Check if a new drone as been deployed + */ + private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof DroneDeployedDiff) { + return { + foreground: async (speed: number) => { + if (speed) { + this.view.audio.playOnce("battle-drone-deploy"); + } + await this.addDrone(diff.drone, speed); + } + } + } else { + return {}; + } + } + + /** + * Check if a drone as been recalled + */ + private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof DroneRecalledDiff) { + return { + foreground: async (speed: number) => { + if (speed) { + this.view.audio.playOnce("battle-drone-destroy"); + } + await this.removeDrone(diff.drone, speed); + } + } + } else { + return {}; + } + } } diff --git a/src/ui/battle/ArenaDrone.ts b/src/ui/battle/ArenaDrone.ts index 92c9bef..6c256a7 100644 --- a/src/ui/battle/ArenaDrone.ts +++ b/src/ui/battle/ArenaDrone.ts @@ -1,77 +1,82 @@ -module TK.SpaceTac.UI { - /** - * Drone sprite in the arena - */ - export class ArenaDrone extends UIContainer { - // Link to view - view: BattleView +import { Drone } from "../../core/Drone" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { UIGraphics } from "../common/UIGraphics" +import { BattleView } from "./BattleView" - // Link to displayed drone - drone: Drone +/** + * Drone sprite in the arena + */ +export class ArenaDrone extends UIContainer { + // Link to view + view: BattleView - // Sprite - sprite: UIButton + // Link to displayed drone + drone: Drone - // Radius - radius: UIGraphics + // Sprite + sprite: UIButton - // Activation effect - activation: UIGraphics + // Radius + radius: UIGraphics - constructor(battleview: BattleView, drone: Drone) { - super(battleview); + // Activation effect + activation: UIGraphics - this.view = battleview; - this.drone = drone; + constructor(battleview: BattleView, drone: Drone) { + super(battleview); - let builder = new UIBuilder(battleview, this); + this.view = battleview; + this.drone = drone; - this.radius = builder.graphics("radius"); - this.radius.fillStyle(0xe9f2f9, 0.1); - this.radius.fillCircle(0, 0, drone.radius); - this.radius.lineStyle(2, 0xe9f2f9, 0.5); - this.radius.strokeCircle(0, 0, drone.radius); + let builder = new UIBuilder(battleview, this); - this.activation = builder.graphics("activation", 0, 0, false); - this.activation.fillStyle(0xe9f2f9, 0.0); - this.activation.fillCircle(0, 0, drone.radius); - this.activation.lineStyle(2, 0xe9f2f9, 0.7); - this.activation.strokeCircle(0, 0, drone.radius); + this.radius = builder.graphics("radius"); + this.radius.fillStyle(0xe9f2f9, 0.1); + this.radius.fillCircle(0, 0, drone.radius); + this.radius.lineStyle(2, 0xe9f2f9, 0.5); + this.radius.strokeCircle(0, 0, drone.radius); - this.sprite = builder.button(`action-${drone.code}`, 0, 0, undefined, () => this.drone.getDescription(), undefined, { center: true }); - this.sprite.setScale(0.1, 0.1); - } + this.activation = builder.graphics("activation", 0, 0, false); + this.activation.fillStyle(0xe9f2f9, 0.0); + this.activation.fillCircle(0, 0, drone.radius); + this.activation.lineStyle(2, 0xe9f2f9, 0.7); + this.activation.strokeCircle(0, 0, drone.radius); - /** - * Start the activation animation - * - * Return the animation duration - */ - setApplied(): number { - this.activation.setScale(0.001, 0.001); - this.activation.visible = true; - let tween = this.view.animations.addAnimation(this.activation, { scaleX: 1, scaleY: 1 }, 500).then(() => this.activation.setVisible(false)); - return 500; - } + this.sprite = builder.button(`action-${drone.code}`, 0, 0, undefined, () => this.drone.getDescription(), undefined, { center: true }); + this.sprite.setScale(0.1, 0.1); + } - /** - * Set the sprite as destroyed - * - * Return the animation duration - */ - async setDestroyed(speed = 1): Promise { - if (speed) { - this.view.animations.addAnimation(this, { alpha: 0.3 }, 300 / speed, undefined, 200 / speed); - await this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500 / speed); - } - this.destroy(); - } + /** + * Start the activation animation + * + * Return the animation duration + */ + setApplied(): number { + this.activation.setScale(0.001, 0.001); + this.activation.visible = true; + let tween = this.view.animations.addAnimation(this.activation, { scaleX: 1, scaleY: 1 }, 500).then(() => this.activation.setVisible(false)); + return 500; + } - /** - * Set the tactical mode display - */ - setTacticalMode(active: boolean) { - this.sprite.setScale(active ? 0.2 : 0.1); - } + /** + * Set the sprite as destroyed + * + * Return the animation duration + */ + async setDestroyed(speed = 1): Promise { + if (speed) { + this.view.animations.addAnimation(this, { alpha: 0.3 }, 300 / speed, undefined, 200 / speed); + await this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500 / speed); } + this.destroy(); + } + + /** + * Set the tactical mode display + */ + setTacticalMode(active: boolean) { + this.sprite.setScale(active ? 0.2 : 0.1); + } } diff --git a/src/ui/battle/ArenaShip.spec.ts b/src/ui/battle/ArenaShip.spec.ts index fb8de62..dd29aef 100644 --- a/src/ui/battle/ArenaShip.spec.ts +++ b/src/ui/battle/ArenaShip.spec.ts @@ -1,43 +1,49 @@ -module TK.SpaceTac.UI.Specs { - testing("ArenaShip", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { ShipAttributeDiff } from "../../core/diffs/ShipAttributeDiff"; +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "../../core/diffs/ShipEffectAddedDiff"; +import { BaseEffect } from "../../core/effects/BaseEffect"; +import { StickyEffect } from "../../core/effects/StickyEffect"; +import { collectTexts, setupBattleview } from "../TestGame"; - test.case("adds effects display", check => { - let ship = nn(testgame.view.battle.playing_ship); - let sprite = nn(testgame.view.arena.findShipSprite(ship)); +testing("ArenaShip", test => { + let testgame = setupBattleview(test); - check.equals(sprite.effects_messages.list.length, 0); + test.case("adds effects display", check => { + let ship = nn(testgame.view.battle.playing_ship); + let sprite = nn(testgame.view.arena.findShipSprite(ship)); - sprite.displayAttributeChanged(new ShipAttributeDiff(ship, "power_capacity", { cumulative: -4 }, {})); + check.equals(sprite.effects_messages.list.length, 0); - check.equals(sprite.effects_messages.list.length, 1); - check.equals(collectTexts(sprite.effects_messages), ["power capacity -4"]); - }); + sprite.displayAttributeChanged(new ShipAttributeDiff(ship, "power_capacity", { cumulative: -4 }, {})); - test.case("adds sticky effects display", check => { - let battle = testgame.view.actual_battle; - let ship = nn(battle.playing_ship); - let sprite = nn(testgame.view.arena.findShipSprite(ship)); + check.equals(sprite.effects_messages.list.length, 1); + check.equals(collectTexts(sprite.effects_messages), ["power capacity -4"]); + }); - check.equals(sprite.active_effects_display.list.length, 0); + test.case("adds sticky effects display", check => { + let battle = testgame.view.actual_battle; + let ship = nn(battle.playing_ship); + let sprite = nn(testgame.view.arena.findShipSprite(ship)); - let effect1 = new StickyEffect(new BaseEffect("test")); - battle.applyDiffs([new ShipEffectAddedDiff(ship, effect1)]); - testgame.view.log_processor.processPending(); - check.equals(sprite.active_effects_display.list.length, 1); + check.equals(sprite.active_effects_display.list.length, 0); - let effect2 = new StickyEffect(new BaseEffect("test")); - battle.applyDiffs([new ShipEffectAddedDiff(ship, effect2)]); - testgame.view.log_processor.processPending(); - check.equals(sprite.active_effects_display.list.length, 2); + let effect1 = new StickyEffect(new BaseEffect("test")); + battle.applyDiffs([new ShipEffectAddedDiff(ship, effect1)]); + testgame.view.log_processor.processPending(); + check.equals(sprite.active_effects_display.list.length, 1); - battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect1)]); - testgame.view.log_processor.processPending(); - check.equals(sprite.active_effects_display.list.length, 1); + let effect2 = new StickyEffect(new BaseEffect("test")); + battle.applyDiffs([new ShipEffectAddedDiff(ship, effect2)]); + testgame.view.log_processor.processPending(); + check.equals(sprite.active_effects_display.list.length, 2); - battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect2)]); - testgame.view.log_processor.processPending(); - check.equals(sprite.active_effects_display.list.length, 0); - }); - }); -} + battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect1)]); + testgame.view.log_processor.processPending(); + check.equals(sprite.active_effects_display.list.length, 1); + + battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect2)]); + testgame.view.log_processor.processPending(); + check.equals(sprite.active_effects_display.list.length, 0); + }); +}); diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index a0293bd..a9f5079 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -1,481 +1,506 @@ -module TK.SpaceTac.UI { - /** - * Ship sprite in the arena, with corresponding HUD - */ - export class ArenaShip extends UIContainer { - // Link to the view - arena: Arena - battleview: BattleView +import { Toggle } from "../../common/Toggle" +import { range } from "../../common/Tools" +import { EndTurnAction } from "../../core/actions/EndTurnAction" +import { ToggleAction } from "../../core/actions/ToggleAction" +import { VigilanceAction } from "../../core/actions/VigilanceAction" +import { BaseBattleDiff, BaseBattleShipDiff } from "../../core/diffs/BaseBattleDiff" +import { ShipActionToggleDiff } from "../../core/diffs/ShipActionToggleDiff" +import { ShipActionUsedDiff } from "../../core/diffs/ShipActionUsedDiff" +import { ShipAttributeDiff } from "../../core/diffs/ShipAttributeDiff" +import { ShipChangeDiff } from "../../core/diffs/ShipChangeDiff" +import { ShipDamageDiff } from "../../core/diffs/ShipDamageDiff" +import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "../../core/diffs/ShipEffectAddedDiff" +import { ShipMoveDiff } from "../../core/diffs/ShipMoveDiff" +import { ShipValueDiff } from "../../core/diffs/ShipValueDiff" +import { VigilanceAppliedDiff } from "../../core/diffs/VigilanceAppliedDiff" +import { Ship } from "../../core/Ship" +import { SHIP_VALUES_NAMES } from "../../core/ShipValue" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIGraphics } from "../common/UIGraphics" +import { UIImage } from "../common/UIImage" +import { UIText } from "../common/UIText" +import { UITools } from "../common/UITools" +import { Arena } from "./Arena" +import { BattleView } from "./BattleView" +import { LogProcessorDelegate } from "./LogProcessor" - // Link to displayed ship - ship: Ship +/** + * Ship sprite in the arena, with corresponding HUD + */ +export class ArenaShip extends UIContainer { + // Link to the view + arena: Arena + battleview: BattleView - // Boolean to indicate if it is an enemy ship - enemy: boolean + // Link to displayed ship + ship: Ship - // Ship sprite - sprite: UIImage + // Boolean to indicate if it is an enemy ship + enemy: boolean - // Stasis effect - stasis: UIImage + // Ship sprite + sprite: UIImage - // HSP display - hsp: UIContainer - power_text: UIText - life_hull: UIContainer - life_shield: UIContainer - life_evasion: UIContainer - toggle_hsp: Toggle + // Stasis effect + stasis: UIImage - // Play order - play_order_container: UIContainer - play_order: UIText - toggle_play_order: Toggle + // HSP display + hsp: UIContainer + power_text: UIText + life_hull: UIContainer + life_shield: UIContainer + life_evasion: UIContainer + toggle_hsp: Toggle - // Frames to indicate the owner, if the ship is hovered, and if it is hovered - frame_owner: UIImage - frame_hover: UIImage + // Play order + play_order_container: UIContainer + play_order: UIText + toggle_play_order: Toggle - // Effects display - active_effects_display: UIContainer - effects_radius: UIGraphics - effects_messages: UIContainer - effects_messages_toggle: Toggle + // Frames to indicate the owner, if the ship is hovered, and if it is hovered + frame_owner: UIImage + frame_hover: UIImage - // Create a ship sprite usable in the Arena - constructor(parent: Arena, ship: Ship) { - super(parent.view); - this.arena = parent; - this.battleview = parent.view; + // Effects display + active_effects_display: UIContainer + effects_radius: UIGraphics + effects_messages: UIContainer + effects_messages_toggle: Toggle - let builder = new UIBuilder(this.battleview).in(this); + // Create a ship sprite usable in the Arena + constructor(parent: Arena, ship: Ship) { + super(parent.view); + this.arena = parent; + this.battleview = parent.view; - this.ship = ship; - this.enemy = !this.battleview.player.is(this.ship.fleet.player); + let builder = new UIBuilder(this.battleview).in(this); - // Add effects radius - this.effects_radius = builder.graphics("effect-radius"); + this.ship = ship; + this.enemy = !this.battleview.player.is(this.ship.fleet.player); - // Add frame indicating which side this ship is on - this.frame_owner = builder.image(this.enemy ? "battle-hud-ship-enemy" : "battle-hud-ship-own", 0, 0, true); - this.setPlaying(false); - this.frame_hover = builder.image("battle-hud-ship-hover", 0, 0, true); - this.frame_hover.setVisible(false); + // Add effects radius + this.effects_radius = builder.graphics("effect-radius"); - // Add ship sprite - this.sprite = builder.image(`ship-${ship.model.code}-sprite`, 0, 0, true); - this.sprite.setRotation(ship.arena_angle); + // Add frame indicating which side this ship is on + this.frame_owner = builder.image(this.enemy ? "battle-hud-ship-enemy" : "battle-hud-ship-own", 0, 0, true); + this.setPlaying(false); + this.frame_hover = builder.image("battle-hud-ship-hover", 0, 0, true); + this.frame_hover.setVisible(false); - // Add stasis effect - this.stasis = builder.image("battle-hud-ship-stasis", 0, 0, true); - this.stasis.setAlpha(0.9); - this.stasis.setVisible(!ship.alive); + // Add ship sprite + this.sprite = builder.image(`ship-${ship.model.code}-sprite`, 0, 0, true); + this.sprite.setRotation(ship.arena_angle); - // HSP display - this.hsp = builder.container("hsp", 0, 34); - builder.in(this.hsp).image("battle-hud-hsp-background", 0, 0, true); - this.power_text = builder.in(this.hsp).text(`${ship.getValue("power")}`, -42, 0, - { size: 13, color: "#ffdd4b", bold: true, shadow: true, center: true }); - this.life_hull = builder.in(this.hsp).container("hull"); - this.life_shield = builder.in(this.hsp).container("shield"); - this.life_evasion = builder.in(this.hsp).container("evasion"); - this.toggle_hsp = this.battleview.animations.newVisibilityToggle(this.hsp, 200, false); + // Add stasis effect + this.stasis = builder.image("battle-hud-ship-stasis", 0, 0, true); + this.stasis.setAlpha(0.9); + this.stasis.setVisible(!ship.alive); - // Play order display - this.play_order_container = builder.container("play_order", -44, 0); - builder.in(this.play_order_container).image("battle-hud-ship-play-order", 0, 0, true); - this.play_order = builder.in(this.play_order_container).text("", -2, 1, { - size: 12, bold: true, color: "#d1d1d1", shadow: true, center: true - }); - this.toggle_play_order = this.battleview.animations.newVisibilityToggle(this.play_order_container, 200, false); + // HSP display + this.hsp = builder.container("hsp", 0, 34); + builder.in(this.hsp).image("battle-hud-hsp-background", 0, 0, true); + this.power_text = builder.in(this.hsp).text(`${ship.getValue("power")}`, -42, 0, + { size: 13, color: "#ffdd4b", bold: true, shadow: true, center: true }); + this.life_hull = builder.in(this.hsp).container("hull"); + this.life_shield = builder.in(this.hsp).container("shield"); + this.life_evasion = builder.in(this.hsp).container("evasion"); + this.toggle_hsp = this.battleview.animations.newVisibilityToggle(this.hsp, 200, false); - // Effects display - this.active_effects_display = builder.container("active-effects", 0, -44); - this.effects_messages = builder.container("effects-messages"); - this.effects_messages_toggle = this.battleview.animations.newVisibilityToggle(this.effects_messages, 500, false); + // Play order display + this.play_order_container = builder.container("play_order", -44, 0); + builder.in(this.play_order_container).image("battle-hud-ship-play-order", 0, 0, true); + this.play_order = builder.in(this.play_order_container).text("", -2, 1, { + size: 12, bold: true, color: "#d1d1d1", shadow: true, center: true + }); + this.toggle_play_order = this.battleview.animations.newVisibilityToggle(this.play_order_container, 200, false); - this.updatePlayOrder(); - this.updateHull(this.ship.getValue("hull")); - this.updateShield(this.ship.getValue("shield")); - this.updateEvasion(this.ship.getAttribute("evasion")); - this.updateActiveEffects(); - this.updateEffectsRadius(); + // Effects display + this.active_effects_display = builder.container("active-effects", 0, -44); + this.effects_messages = builder.container("effects-messages"); + this.effects_messages_toggle = this.battleview.animations.newVisibilityToggle(this.effects_messages, 500, false); - // Set location - if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && this.battleview.player.is(ship.fleet.player)) { - this.setPosition(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle)); - this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 1); - } else { - this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 0); - } + this.updatePlayOrder(); + this.updateHull(this.ship.getValue("hull")); + this.updateShield(this.ship.getValue("shield")); + this.updateEvasion(this.ship.getAttribute("evasion")); + this.updateActiveEffects(); + this.updateEffectsRadius(); - // Log processing - this.battleview.log_processor.register(diff => this.processBattleDiff(diff)); - this.battleview.log_processor.registerForShip(ship, diff => this.processShipDiff(diff)); - } - - jasmineToString(): string { - return `ArenaShip ${this.ship.jasmineToString()}`; - } - - /** - * Process a battle diff - */ - private processBattleDiff(diff: BaseBattleDiff) { - if (diff instanceof ShipChangeDiff) { - this.updatePlayOrder(); - } - return {}; - } - - /** - * Process a ship diff - */ - private processShipDiff(diff: BaseBattleShipDiff): LogProcessorDelegate { - let timer = this.battleview.timer; - - if (diff instanceof ShipEffectAddedDiff || diff instanceof ShipEffectRemovedDiff) { - return { - background: async () => this.updateActiveEffects() - } - } else if (diff instanceof ShipValueDiff) { - return { - background: async (speed: number) => { - if (speed) { - this.toggle_hsp.manipulate("value")(true); - } - - if (diff.code == "hull") { - if (speed) { - this.updateHull(this.ship.getValue("hull") - diff.diff, diff.diff); - await timer.sleep(1000 / speed); - this.updateHull(this.ship.getValue("hull")); - await timer.sleep(500 / speed); - } else { - this.updateHull(this.ship.getValue("hull")); - } - } else if (diff.code == "shield") { - if (speed) { - this.updateShield(this.ship.getValue("shield") - diff.diff, diff.diff); - await timer.sleep(1000 / speed); - this.updateShield(this.ship.getValue("shield")); - await timer.sleep(500 / speed); - } else { - this.updateShield(this.ship.getValue("shield")); - } - } else if (diff.code == "power") { - this.power_text.setText(`${this.ship.getValue("power")}`); - if (speed) { - await this.battleview.animations.blink(this.power_text, { speed: speed }); - } - } - - if (speed) { - await timer.sleep(500 / speed); - this.toggle_hsp.manipulate("value")(false); - } - } - } - } else if (diff instanceof ShipAttributeDiff) { - return { - background: async (speed: number) => { - if (speed) { - this.displayAttributeChanged(diff, speed); - if (diff.code == "evasion") { - // TODO diff - this.updateEvasion(this.ship.getAttribute("evasion")); - this.toggle_hsp.manipulate("attribute")(2000 / speed); - } - await timer.sleep(2000 / speed); - } - } - } - } else if (diff instanceof ShipDamageDiff) { - return { - background: async (speed: number) => { - if (speed) { - await this.displayEffect(`${diff.theoretical} damage`, false, speed); - await timer.sleep(1000 / speed); - } - } - } - } else if (diff instanceof ShipActionToggleDiff) { - return { - foreground: async (speed: number) => { - let action = this.ship.actions.getById(diff.action); - if (action) { - if (speed) { - if (diff.activated) { - await this.displayEffect(`${action.name} ON`, true, speed); - } else { - await this.displayEffect(`${action.name} OFF`, false, speed); - } - } - - this.updateEffectsRadius(); - await timer.sleep(500 / speed); - } - } - } - } else if (diff instanceof ShipActionUsedDiff) { - let action = this.ship.actions.getById(diff.action); - if (action) { - if (action instanceof EndTurnAction) { - return { - foreground: async (speed: number) => { - if (speed) { - await this.displayEffect("End turn", true, speed); - await timer.sleep(500 / speed); - } - } - } - } else if (!(action instanceof ToggleAction)) { - let action_name = action.name; - return { - foreground: async (speed: number) => { - if (speed) { - await this.displayEffect(action_name, true, speed); - await timer.sleep(300 / speed); - } - } - } - } else { - return {}; - } - } else { - return {}; - } - } else if (diff instanceof ShipMoveDiff) { - let func = async (speed: number) => { - if (speed) { - await this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, 0); - await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, speed, !!diff.engine); - } else { - await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, 0); - } - }; - if (diff.engine) { - return { foreground: func }; - } else { - return { background: func }; - } - } else if (diff instanceof VigilanceAppliedDiff) { - let action = this.ship.actions.getById(diff.action); - return { - foreground: async (speed: number) => { - if (speed && action) { - await this.displayEffect(`${action.name} (vigilance)`, true, speed); - await timer.sleep(300 / speed); - } - } - } - } else { - return {}; - } - } - - /** - * Set the hovered state on this ship - * - * This will show the information HUD accordingly - */ - setHovered(hovered: boolean, tactical: boolean) { - let client = tactical ? "tactical" : "hover"; - - if (hovered && this.ship.alive) { - this.toggle_hsp.manipulate(client)(true); - if (tactical) { - this.toggle_play_order.manipulate(client)(true); - } - } else { - this.toggle_hsp.manipulate(client)(false); - this.toggle_play_order.manipulate(client)(false); - } - - this.battleview.animations.setVisible(this.frame_hover, hovered && this.ship.alive && !tactical, 200); - } - - /** - * Set the playing state on this ship - * - * This will alter the HUD frame to show this state - */ - async setPlaying(playing: boolean, animate = true): Promise { - this.frame_owner.alpha = playing ? 1 : 0.35; - this.frame_owner.visible = this.ship.alive; - - if (playing && animate) { - this.battleview.audio.playOnce("battle-ship-change"); - await this.battleview.animations.blink(this.frame_owner); - } - } - - /** - * Activate the dead effect (stasis) - */ - setDead(dead = true) { - if (dead) { - //this.displayEffect("stasis", false); - this.stasis.visible = true; - this.stasis.alpha = 0; - this.battleview.animations.blink(this.stasis, { alpha_on: 0.9, alpha_off: 0.7 }); - } else { - this.stasis.visible = false; - } - this.setPlaying(false); - } - - /** - * Move the sprite to a location - * - * Return the duration of animation - */ - async moveToArenaLocation(x: number, y: number, facing_angle: number, speed = 1, engine = true): Promise { - if (speed) { - if (engine) { - await this.arena.view.animations.moveInSpace(this, x, y, facing_angle, this.sprite, speed); - } else { - await this.arena.view.animations.moveTo(this, x, y, facing_angle, this.sprite, speed); - } - } else { - this.setPosition(x, y); - this.sprite.setRotation(facing_angle); - } - } - - /** - * Briefly show an effect on this ship - */ - async displayEffect(message: string, beneficial: boolean, speed: number) { - if (!this.effects_messages.visible) { - this.effects_messages.removeAll(true); - } - - if (!speed) { - return; - } - - let builder = new UIBuilder(this.arena.view, this.effects_messages); - builder.text(message, 0, 20 * this.effects_messages.length, { - color: beneficial ? "#afe9c6" : "#e9afaf" - }); - - let arena = this.battleview.arena.getBoundaries(); - this.effects_messages.setPosition( - (this.ship.arena_x < 100) ? 0 : ((this.ship.arena_x > arena.width - 100) ? (-this.effects_messages.width) : (-this.effects_messages.width * 0.5)), - (this.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height) - ); - - this.effects_messages_toggle.manipulate("added")(1400 / speed); - await this.battleview.timer.sleep(1500 / speed); - } - - /** - * Display interesting changes in ship attributes - */ - displayAttributeChanged(event: ShipAttributeDiff, speed = 1) { - // TODO show final diff, not just cumulative one - let diff = (event.added.cumulative || 0) - (event.removed.cumulative || 0); - if (diff) { - let name = SHIP_VALUES_NAMES[event.code]; - this.displayEffect(`${name} ${diff < 0 ? "-" : "+"}${Math.abs(diff)}`, diff >= 0, speed); - } - } - - /** - * Update the play order indicator - */ - updatePlayOrder(): void { - let play_order = this.battleview.battle.getPlayOrder(this.ship); - if (play_order == 0) { - this.play_order.setText("-"); - } else { - this.play_order.setText(play_order.toString()); - } - } - - /** - * Reposition the HSP indicators - */ - repositionLifeIndicators(): void { - this.life_hull.x = -25; - this.life_shield.x = this.life_hull.x + (this.life_hull.length * 9); - this.life_evasion.x = this.life_shield.x + (this.life_shield.length * 9); - } - - /** - * Update the hull indicator - */ - updateHull(current: number, diff = 0): void { - let builder = new UIBuilder(this.battleview, this.life_hull); - builder.clear(); - - range(current).forEach(i => { - builder.image("battle-hud-hsp-hull", i * 9, 0, true); - }); - - this.repositionLifeIndicators(); - } - - /** - * Update the shield indicator - */ - updateShield(current: number, diff = 0): void { - let builder = new UIBuilder(this.battleview, this.life_shield); - builder.clear(); - - range(current).forEach(i => { - builder.image("battle-hud-hsp-shield", i * 9, 0, true); - }); - - this.repositionLifeIndicators(); - } - - /** - * Update the evasion indicator - */ - updateEvasion(current: number, diff = 0): void { - let builder = new UIBuilder(this.battleview, this.life_evasion); - builder.clear(); - - range(current).forEach(i => { - builder.image("battle-hud-hsp-evasion", i * 9, 0, true); - }); - - this.repositionLifeIndicators(); - } - - /** - * Update the list of effects active on the ship - */ - updateActiveEffects() { - this.active_effects_display.removeAll(); - - let effects = this.ship.active_effects.list().filter(effect => !effect.isInternal()); - - let count = effects.length; - if (count) { - let positions = UITools.evenlySpace(70, 17, count); - - effects.forEach((effect, index) => { - let name = effect.isBeneficial() ? "battle-hud-ship-effect-good" : "battle-hud-ship-effect-bad"; - let dot = this.battleview.newImage(name, positions[index] - 35, 0); - this.active_effects_display.add(dot); - }); - } - } - - /** - * Update the activated effects radius - */ - updateEffectsRadius(): void { - this.effects_radius.clear(); - this.ship.actions.listToggled().forEach(action => { - let color = (action instanceof VigilanceAction) ? 0xf4bf42 : 0xe9f2f9; - this.effects_radius.lineStyle(2, color, 0.5); - this.effects_radius.fillStyle(color, 0.1); - this.effects_radius.fillCircle(0, 0, action.radius); - }); - } + // Set location + if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && this.battleview.player.is(ship.fleet.player)) { + this.setPosition(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle)); + this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 1); + } else { + this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, 0); } + + // Log processing + this.battleview.log_processor.register(diff => this.processBattleDiff(diff)); + this.battleview.log_processor.registerForShip(ship, diff => this.processShipDiff(diff)); + } + + jasmineToString(): string { + return `ArenaShip ${this.ship.jasmineToString()}`; + } + + /** + * Process a battle diff + */ + private processBattleDiff(diff: BaseBattleDiff) { + if (diff instanceof ShipChangeDiff) { + this.updatePlayOrder(); + } + return {}; + } + + /** + * Process a ship diff + */ + private processShipDiff(diff: BaseBattleShipDiff): LogProcessorDelegate { + let timer = this.battleview.timer; + + if (diff instanceof ShipEffectAddedDiff || diff instanceof ShipEffectRemovedDiff) { + return { + background: async () => this.updateActiveEffects() + } + } else if (diff instanceof ShipValueDiff) { + return { + background: async (speed: number) => { + if (speed) { + this.toggle_hsp.manipulate("value")(true); + } + + if (diff.code == "hull") { + if (speed) { + this.updateHull(this.ship.getValue("hull") - diff.diff, diff.diff); + await timer.sleep(1000 / speed); + this.updateHull(this.ship.getValue("hull")); + await timer.sleep(500 / speed); + } else { + this.updateHull(this.ship.getValue("hull")); + } + } else if (diff.code == "shield") { + if (speed) { + this.updateShield(this.ship.getValue("shield") - diff.diff, diff.diff); + await timer.sleep(1000 / speed); + this.updateShield(this.ship.getValue("shield")); + await timer.sleep(500 / speed); + } else { + this.updateShield(this.ship.getValue("shield")); + } + } else if (diff.code == "power") { + this.power_text.setText(`${this.ship.getValue("power")}`); + if (speed) { + await this.battleview.animations.blink(this.power_text, { speed: speed }); + } + } + + if (speed) { + await timer.sleep(500 / speed); + this.toggle_hsp.manipulate("value")(false); + } + } + } + } else if (diff instanceof ShipAttributeDiff) { + return { + background: async (speed: number) => { + if (speed) { + this.displayAttributeChanged(diff, speed); + if (diff.code == "evasion") { + // TODO diff + this.updateEvasion(this.ship.getAttribute("evasion")); + this.toggle_hsp.manipulate("attribute")(2000 / speed); + } + await timer.sleep(2000 / speed); + } + } + } + } else if (diff instanceof ShipDamageDiff) { + return { + background: async (speed: number) => { + if (speed) { + await this.displayEffect(`${diff.theoretical} damage`, false, speed); + await timer.sleep(1000 / speed); + } + } + } + } else if (diff instanceof ShipActionToggleDiff) { + return { + foreground: async (speed: number) => { + let action = this.ship.actions.getById(diff.action); + if (action) { + if (speed) { + if (diff.activated) { + await this.displayEffect(`${action.name} ON`, true, speed); + } else { + await this.displayEffect(`${action.name} OFF`, false, speed); + } + } + + this.updateEffectsRadius(); + await timer.sleep(500 / speed); + } + } + } + } else if (diff instanceof ShipActionUsedDiff) { + let action = this.ship.actions.getById(diff.action); + if (action) { + if (action instanceof EndTurnAction) { + return { + foreground: async (speed: number) => { + if (speed) { + await this.displayEffect("End turn", true, speed); + await timer.sleep(500 / speed); + } + } + } + } else if (!(action instanceof ToggleAction)) { + let action_name = action.name; + return { + foreground: async (speed: number) => { + if (speed) { + await this.displayEffect(action_name, true, speed); + await timer.sleep(300 / speed); + } + } + } + } else { + return {}; + } + } else { + return {}; + } + } else if (diff instanceof ShipMoveDiff) { + let func = async (speed: number) => { + if (speed) { + await this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, 0); + await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, speed, !!diff.engine); + } else { + await this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, 0); + } + }; + if (diff.engine) { + return { foreground: func }; + } else { + return { background: func }; + } + } else if (diff instanceof VigilanceAppliedDiff) { + let action = this.ship.actions.getById(diff.action); + return { + foreground: async (speed: number) => { + if (speed && action) { + await this.displayEffect(`${action.name} (vigilance)`, true, speed); + await timer.sleep(300 / speed); + } + } + } + } else { + return {}; + } + } + + /** + * Set the hovered state on this ship + * + * This will show the information HUD accordingly + */ + setHovered(hovered: boolean, tactical: boolean) { + let client = tactical ? "tactical" : "hover"; + + if (hovered && this.ship.alive) { + this.toggle_hsp.manipulate(client)(true); + if (tactical) { + this.toggle_play_order.manipulate(client)(true); + } + } else { + this.toggle_hsp.manipulate(client)(false); + this.toggle_play_order.manipulate(client)(false); + } + + this.battleview.animations.setVisible(this.frame_hover, hovered && this.ship.alive && !tactical, 200); + } + + /** + * Set the playing state on this ship + * + * This will alter the HUD frame to show this state + */ + async setPlaying(playing: boolean, animate = true): Promise { + this.frame_owner.alpha = playing ? 1 : 0.35; + this.frame_owner.visible = this.ship.alive; + + if (playing && animate) { + this.battleview.audio.playOnce("battle-ship-change"); + await this.battleview.animations.blink(this.frame_owner); + } + } + + /** + * Activate the dead effect (stasis) + */ + setDead(dead = true) { + if (dead) { + //this.displayEffect("stasis", false); + this.stasis.visible = true; + this.stasis.alpha = 0; + this.battleview.animations.blink(this.stasis, { alpha_on: 0.9, alpha_off: 0.7 }); + } else { + this.stasis.visible = false; + } + this.setPlaying(false); + } + + /** + * Move the sprite to a location + * + * Return the duration of animation + */ + async moveToArenaLocation(x: number, y: number, facing_angle: number, speed = 1, engine = true): Promise { + if (speed) { + if (engine) { + await this.arena.view.animations.moveInSpace(this, x, y, facing_angle, this.sprite, speed); + } else { + await this.arena.view.animations.moveTo(this, x, y, facing_angle, this.sprite, speed); + } + } else { + this.setPosition(x, y); + this.sprite.setRotation(facing_angle); + } + } + + /** + * Briefly show an effect on this ship + */ + async displayEffect(message: string, beneficial: boolean, speed: number) { + if (!this.effects_messages.visible) { + this.effects_messages.removeAll(true); + } + + if (!speed) { + return; + } + + let builder = new UIBuilder(this.arena.view, this.effects_messages); + builder.text(message, 0, 20 * this.effects_messages.length, { + color: beneficial ? "#afe9c6" : "#e9afaf" + }); + + let arena = this.battleview.arena.getBoundaries(); + this.effects_messages.setPosition( + (this.ship.arena_x < 100) ? 0 : ((this.ship.arena_x > arena.width - 100) ? (-this.effects_messages.width) : (-this.effects_messages.width * 0.5)), + (this.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height) + ); + + this.effects_messages_toggle.manipulate("added")(1400 / speed); + await this.battleview.timer.sleep(1500 / speed); + } + + /** + * Display interesting changes in ship attributes + */ + displayAttributeChanged(event: ShipAttributeDiff, speed = 1) { + // TODO show final diff, not just cumulative one + let diff = (event.added.cumulative || 0) - (event.removed.cumulative || 0); + if (diff) { + let name = SHIP_VALUES_NAMES[event.code]; + this.displayEffect(`${name} ${diff < 0 ? "-" : "+"}${Math.abs(diff)}`, diff >= 0, speed); + } + } + + /** + * Update the play order indicator + */ + updatePlayOrder(): void { + let play_order = this.battleview.battle.getPlayOrder(this.ship); + if (play_order == 0) { + this.play_order.setText("-"); + } else { + this.play_order.setText(play_order.toString()); + } + } + + /** + * Reposition the HSP indicators + */ + repositionLifeIndicators(): void { + this.life_hull.x = -25; + this.life_shield.x = this.life_hull.x + (this.life_hull.length * 9); + this.life_evasion.x = this.life_shield.x + (this.life_shield.length * 9); + } + + /** + * Update the hull indicator + */ + updateHull(current: number, diff = 0): void { + let builder = new UIBuilder(this.battleview, this.life_hull); + builder.clear(); + + range(current).forEach(i => { + builder.image("battle-hud-hsp-hull", i * 9, 0, true); + }); + + this.repositionLifeIndicators(); + } + + /** + * Update the shield indicator + */ + updateShield(current: number, diff = 0): void { + let builder = new UIBuilder(this.battleview, this.life_shield); + builder.clear(); + + range(current).forEach(i => { + builder.image("battle-hud-hsp-shield", i * 9, 0, true); + }); + + this.repositionLifeIndicators(); + } + + /** + * Update the evasion indicator + */ + updateEvasion(current: number, diff = 0): void { + let builder = new UIBuilder(this.battleview, this.life_evasion); + builder.clear(); + + range(current).forEach(i => { + builder.image("battle-hud-hsp-evasion", i * 9, 0, true); + }); + + this.repositionLifeIndicators(); + } + + /** + * Update the list of effects active on the ship + */ + updateActiveEffects() { + this.active_effects_display.removeAll(); + + let effects = this.ship.active_effects.list().filter(effect => !effect.isInternal()); + + let count = effects.length; + if (count) { + let positions = UITools.evenlySpace(70, 17, count); + + effects.forEach((effect, index) => { + let name = effect.isBeneficial() ? "battle-hud-ship-effect-good" : "battle-hud-ship-effect-bad"; + let dot = this.battleview.newImage(name, positions[index] - 35, 0); + this.active_effects_display.add(dot); + }); + } + } + + /** + * Update the activated effects radius + */ + updateEffectsRadius(): void { + this.effects_radius.clear(); + this.ship.actions.listToggled().forEach(action => { + let color = (action instanceof VigilanceAction) ? 0xf4bf42 : 0xe9f2f9; + this.effects_radius.lineStyle(2, color, 0.5); + this.effects_radius.fillStyle(color, 0.1); + this.effects_radius.fillCircle(0, 0, action.radius); + }); + } } diff --git a/src/ui/battle/BattleSplash.ts b/src/ui/battle/BattleSplash.ts index d67c4da..16a7322 100644 --- a/src/ui/battle/BattleSplash.ts +++ b/src/ui/battle/BattleSplash.ts @@ -1,110 +1,114 @@ -module TK.SpaceTac.UI { - /** - * Splash screen at the start of battle - */ - export class BattleSplash { - constructor(private view: BaseView, private fleet1: Fleet, private fleet2: Fleet) { +import { Fleet } from "../../core/Fleet"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIContainer } from "../common/UIContainer"; +import { ShipTooltip } from "./ShipTooltip"; + +/** + * Splash screen at the start of battle + */ +export class BattleSplash { + constructor(private view: BaseView, private fleet1: Fleet, private fleet2: Fleet) { + } + + /** + * Create and animate splash component, returns when the animation is ended + */ + private async components(builder: UIBuilder, container: UIContainer): Promise { + container.setScale(0.8); + + builder.image("battle-splash-message-off", 0, 0, true); + + let message = builder.image("battle-splash-message-on", 0, 0, true); + message.visible = false; + + let player1 = builder.container("player1", 0, -50, false); + builder.in(player1, builder => { + builder.image("battle-splash-moving-part", 0, 0, true); + + let player1_name = builder.text(this.fleet1.name, -224, 0, { size: 22, bold: true, color: "#154d13" }); + player1_name.angle = -48; + + this.fleet1.ships.forEach((ship, index) => { + let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true); + let ship_portrait = builder.in(ship_card).button(`ship-${ship.model.code}-portrait`, 0, 0, undefined, filler => ShipTooltip.fillInfo(filler, ship), undefined, { + center: true + }); + ship_portrait.setScale(0.3); + }); + }); + + let player2 = builder.container("player2", 0, 50, false); + player2.setAngle(180); + builder.in(player2, builder => { + builder.image("battle-splash-moving-part", 0, 0, true); + + let player2_name = builder.text(this.fleet2.name, -224, 0, { size: 22, bold: true, color: "#651713" }); + player2_name.angle = -228; + + this.fleet2.ships.forEach((ship, index) => { + let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true); + let ship_portrait = builder.in(ship_card).button(`ship-${ship.model.code}-portrait`, 0, 0, undefined, filler => ShipTooltip.fillInfo(filler, ship), undefined, { + center: true + }); + ship_portrait.setAngle(180); + ship_portrait.setScale(0.3); + }); + }); + + // Animations + let anims = this.view.animations; + + await anims.addAnimation(container, { scaleX: 1, scaleY: 1 }, 300, 'Bounce.easeOut'); + + this.view.timer.schedule(600, () => { + message.visible = true; + message.alpha = 0.7; + }); + this.view.timer.schedule(660, () => message.alpha = 0.1); + this.view.timer.schedule(680, () => message.alpha = 0.8); + this.view.timer.schedule(710, () => message.alpha = 0.3); + this.view.timer.schedule(760, () => message.alpha = 1); + + player1.x = -2000; + player1.visible = true; + player2.x = 2000; + player2.visible = true; + anims.addAnimation(player2, { x: 147 }, 600, 'Bounce.easeOut', 400); + await anims.addAnimation(player1, { x: -150 }, 600, 'Bounce.easeOut', 400); + } + + /** + * Create an overlay, returns when it is clicked + */ + overlay(builder: UIBuilder): Promise { + return new Promise(resolve => { + let overlay = builder.overlay({ + color: 0x000000, + alpha: 0.5, + on_click: () => { + overlay.destroy(); + resolve(); } + }); + }); + } - /** - * Create and animate splash component, returns when the animation is ended - */ - private async components(builder: UIBuilder, container: UIContainer): Promise { - container.setScale(0.8); + /** + * Start the animation + */ + start(parent?: UIContainer): Promise { + let builder = new UIBuilder(this.view, parent); + let overlay = this.overlay(builder); - builder.image("battle-splash-message-off", 0, 0, true); + let container = builder.container("splash", this.view.getMidWidth(), this.view.getMidHeight()); + let components = this.components(builder.in(container), container); - let message = builder.image("battle-splash-message-on", 0, 0, true); - message.visible = false; - - let player1 = builder.container("player1", 0, -50, false); - builder.in(player1, builder => { - builder.image("battle-splash-moving-part", 0, 0, true); - - let player1_name = builder.text(this.fleet1.name, -224, 0, { size: 22, bold: true, color: "#154d13" }); - player1_name.angle = -48; - - this.fleet1.ships.forEach((ship, index) => { - let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true); - let ship_portrait = builder.in(ship_card).button(`ship-${ship.model.code}-portrait`, 0, 0, undefined, filler => ShipTooltip.fillInfo(filler, ship), undefined, { - center: true - }); - ship_portrait.setScale(0.3); - }); - }); - - let player2 = builder.container("player2", 0, 50, false); - player2.setAngle(180); - builder.in(player2, builder => { - builder.image("battle-splash-moving-part", 0, 0, true); - - let player2_name = builder.text(this.fleet2.name, -224, 0, { size: 22, bold: true, color: "#651713" }); - player2_name.angle = -228; - - this.fleet2.ships.forEach((ship, index) => { - let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true); - let ship_portrait = builder.in(ship_card).button(`ship-${ship.model.code}-portrait`, 0, 0, undefined, filler => ShipTooltip.fillInfo(filler, ship), undefined, { - center: true - }); - ship_portrait.setAngle(180); - ship_portrait.setScale(0.3); - }); - }); - - // Animations - let anims = this.view.animations; - - await anims.addAnimation(container, { scaleX: 1, scaleY: 1 }, 300, 'Bounce.easeOut'); - - this.view.timer.schedule(600, () => { - message.visible = true; - message.alpha = 0.7; - }); - this.view.timer.schedule(660, () => message.alpha = 0.1); - this.view.timer.schedule(680, () => message.alpha = 0.8); - this.view.timer.schedule(710, () => message.alpha = 0.3); - this.view.timer.schedule(760, () => message.alpha = 1); - - player1.x = -2000; - player1.visible = true; - player2.x = 2000; - player2.visible = true; - anims.addAnimation(player2, { x: 147 }, 600, 'Bounce.easeOut', 400); - await anims.addAnimation(player1, { x: -150 }, 600, 'Bounce.easeOut', 400); - } - - /** - * Create an overlay, returns when it is clicked - */ - overlay(builder: UIBuilder): Promise { - return new Promise(resolve => { - let overlay = builder.overlay({ - color: 0x000000, - alpha: 0.5, - on_click: () => { - overlay.destroy(); - resolve(); - } - }); - }); - } - - /** - * Start the animation - */ - start(parent?: UIContainer): Promise { - let builder = new UIBuilder(this.view, parent); - let overlay = this.overlay(builder); - - let container = builder.container("splash", this.view.getMidWidth(), this.view.getMidHeight()); - let components = this.components(builder.in(container), container); - - return Promise.all([ - overlay.then(() => container.setVisible(false)), - components - ]).then(() => { - container.destroy(); - }); - } - } + return Promise.all([ + overlay.then(() => container.setVisible(false)), + components + ]).then(() => { + container.destroy(); + }); + } } diff --git a/src/ui/battle/BattleView.spec.ts b/src/ui/battle/BattleView.spec.ts index 006d794..2e9d144 100644 --- a/src/ui/battle/BattleView.spec.ts +++ b/src/ui/battle/BattleView.spec.ts @@ -1,89 +1,95 @@ -module TK.SpaceTac.UI.Specs { - testing("BattleView", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { nn } from "../../common/Tools"; +import { ActionTargettingMode } from "../../core/actions/BaseAction"; +import { ArenaLocation } from "../../core/ArenaLocation"; +import { Target } from "../../core/Target"; +import { TestTools } from "../../core/TestTools"; +import { setupBattleview } from "../TestGame"; - test.case("handles ship hovering to display tooltip", check => { - let battleview = testgame.view; - check.equals(battleview.ship_hovered, null, "initial state"); +testing("BattleView", test => { + let testgame = setupBattleview(test); - let ship = nn(battleview.battle.playing_ship); - battleview.cursorHovered(ship.location, ship); - check.same(battleview.ship_hovered, ship, "ship1 hovered"); + test.case("handles ship hovering to display tooltip", check => { + let battleview = testgame.view; + check.equals(battleview.ship_hovered, null, "initial state"); - ship = nn(battleview.battle.play_order[1]); - battleview.cursorHovered(ship.location, ship); - check.same(battleview.ship_hovered, ship, "ship2 hovered"); + let ship = nn(battleview.battle.playing_ship); + battleview.cursorHovered(ship.location, ship); + check.same(battleview.ship_hovered, ship, "ship1 hovered"); - battleview.cursorHovered(new ArenaLocation(0, 0), null); - check.equals(battleview.ship_hovered, null, "out"); + ship = nn(battleview.battle.play_order[1]); + battleview.cursorHovered(ship.location, ship); + check.same(battleview.ship_hovered, ship, "ship2 hovered"); - battleview.cursorOnShip(ship); - check.same(battleview.ship_hovered, ship, "force on"); + battleview.cursorHovered(new ArenaLocation(0, 0), null); + check.equals(battleview.ship_hovered, null, "out"); - battleview.cursorOffShip(battleview.battle.play_order[2]); - check.same(battleview.ship_hovered, ship, "force off on wrong ship"); + battleview.cursorOnShip(ship); + check.same(battleview.ship_hovered, ship, "force on"); - battleview.cursorOffShip(ship); - check.equals(battleview.ship_hovered, null, "force off"); - }); + battleview.cursorOffShip(battleview.battle.play_order[2]); + check.same(battleview.ship_hovered, ship, "force off on wrong ship"); - test.case("forwards cursor hovering and click to targetting", check => { - let battleview = testgame.view; - check.equals(battleview.targetting.active, false); - battleview.setInteractionEnabled(true); + battleview.cursorOffShip(ship); + check.equals(battleview.ship_hovered, null, "force off"); + }); - let ship = nn(battleview.battle.playing_ship); - let weapon = TestTools.addWeapon(ship, 10); - battleview.enterTargettingMode(ship, weapon, ActionTargettingMode.SPACE); - check.equals(battleview.targetting.active, true); + test.case("forwards cursor hovering and click to targetting", check => { + let battleview = testgame.view; + check.equals(battleview.targetting.active, false); + battleview.setInteractionEnabled(true); - battleview.cursorHovered(new ArenaLocation(5, 8), null); - check.equals(battleview.targetting.target, Target.newFromLocation(5, 8)); - check.equals(battleview.ship_hovered, null); + let ship = nn(battleview.battle.playing_ship); + let weapon = TestTools.addWeapon(ship, 10); + battleview.enterTargettingMode(ship, weapon, ActionTargettingMode.SPACE); + check.equals(battleview.targetting.active, true); - ship = battleview.battle.play_order[3]; - battleview.cursorHovered(ship.location, ship); - check.equals(battleview.targetting.target, Target.newFromLocation(ship.arena_x, ship.arena_y)); - check.equals(battleview.ship_hovered, null); + battleview.cursorHovered(new ArenaLocation(5, 8), null); + check.equals(battleview.targetting.target, Target.newFromLocation(5, 8)); + check.equals(battleview.ship_hovered, null); - let validate = check.patch(battleview.targetting, "validate", null); + ship = battleview.battle.play_order[3]; + battleview.cursorHovered(ship.location, ship); + check.equals(battleview.targetting.target, Target.newFromLocation(ship.arena_x, ship.arena_y)); + check.equals(battleview.ship_hovered, null); - check.called(validate, 0); - battleview.cursorClicked(); - check.called(validate, 1); + let validate = check.patch(battleview.targetting, "validate", null); - battleview.exitTargettingMode(); - check.equals(battleview.targetting.active, false); + check.called(validate, 0); + battleview.cursorClicked(); + check.called(validate, 1); - battleview.cursorHovered(new ArenaLocation(5, 8), null); - check.equals(battleview.targetting.target, null); - check.equals(battleview.ship_hovered, null); + battleview.exitTargettingMode(); + check.equals(battleview.targetting.active, false); - battleview.cursorHovered(ship.location, ship); - check.equals(battleview.targetting.target, null); - check.same(battleview.ship_hovered, ship); - }); + battleview.cursorHovered(new ArenaLocation(5, 8), null); + check.equals(battleview.targetting.target, null); + check.equals(battleview.ship_hovered, null); - test.case("allows to choose an action and a target with shortcut keys", check => { - let battleview = testgame.view; - battleview.setInteractionEnabled(true); - let action_icon = battleview.action_bar.action_icons[0]; + battleview.cursorHovered(ship.location, ship); + check.equals(battleview.targetting.target, null); + check.same(battleview.ship_hovered, ship); + }); - check.equals(battleview.targetting.active, false); - check.equals(battleview.action_bar.hasActionSelected(), false); - battleview.numberPressed(battleview.action_bar.action_icons.indexOf(action_icon) + 1); - check.equals(battleview.action_bar.hasActionSelected(), true); - check.equals(battleview.targetting.active, true); - check.same(battleview.targetting.action, action_icon.action); - check.equals(battleview.targetting.target, action_icon.action.getDefaultTarget(action_icon.ship)); - battleview.numberPressed(3); - check.equals(battleview.targetting.active, true); - check.same(battleview.targetting.action, action_icon.action); - check.equals(battleview.targetting.target, Target.newFromShip(battleview.battle.play_order[3])); - battleview.numberPressed(4); - check.equals(battleview.targetting.active, true); - check.same(battleview.targetting.action, action_icon.action); - check.equals(battleview.targetting.target, Target.newFromShip(battleview.battle.play_order[4])); - }); - }); -} + test.case("allows to choose an action and a target with shortcut keys", check => { + let battleview = testgame.view; + battleview.setInteractionEnabled(true); + let action_icon = battleview.action_bar.action_icons[0]; + + check.equals(battleview.targetting.active, false); + check.equals(battleview.action_bar.hasActionSelected(), false); + battleview.numberPressed(battleview.action_bar.action_icons.indexOf(action_icon) + 1); + check.equals(battleview.action_bar.hasActionSelected(), true); + check.equals(battleview.targetting.active, true); + check.same(battleview.targetting.action, action_icon.action); + check.equals(battleview.targetting.target, action_icon.action.getDefaultTarget(action_icon.ship)); + battleview.numberPressed(3); + check.equals(battleview.targetting.active, true); + check.same(battleview.targetting.action, action_icon.action); + check.equals(battleview.targetting.target, Target.newFromShip(battleview.battle.play_order[3])); + battleview.numberPressed(4); + check.equals(battleview.targetting.active, true); + check.same(battleview.targetting.action, action_icon.action); + check.equals(battleview.targetting.target, Target.newFromShip(battleview.battle.play_order[4])); + }); +}); diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index e169664..334e171 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -1,386 +1,407 @@ -/// +import { NAMESPACE } from "../.." +import { ifirst } from "../../common/Iterators" +import { Toggle } from "../../common/Toggle" +import { bound, duplicate, nn, range } from "../../common/Tools" +import { ActionTargettingMode, BaseAction } from "../../core/actions/BaseAction" +import { ArenaLocation } from "../../core/ArenaLocation" +import { Battle } from "../../core/Battle" +import { Player } from "../../core/Player" +import { Ship } from "../../core/Ship" +import { Target } from "../../core/Target" +import { BaseView } from "../BaseView" +import { CharacterSheet, CharacterSheetMode } from "../character/CharacterSheet" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { ActionBar } from "./ActionBar" +import { Arena } from "./Arena" +import { BattleSplash } from "./BattleSplash" +import { LogProcessor } from "./LogProcessor" +import { MultiBattle } from "./MultiBattle" +import { OutcomeDialog } from "./OutcomeDialog" +import { ShipList } from "./ShipList" +import { ShipTooltip } from "./ShipTooltip" +import { Targetting } from "./Targetting" -module TK.SpaceTac.UI { - /** - * Interface for interacting with a ship (hover and click) - */ - export interface IShipButton { - cursorOnShip: (ship: Ship) => void; - cursorOffShip: (ship: Ship) => void; - cursorClicked: () => void; - } - - /** - * Interactive view of a Battle - */ - export class BattleView extends BaseView implements IShipButton { - // Internal battle state - actual_battle!: Battle - - // Displayed battle state - battle!: Battle - - // Interacting player - player!: Player - - // Multiplayer sharing - multi!: MultiBattle - - // Layers - layer_background!: UIContainer - layer_arena!: UIContainer - layer_borders!: UIContainer - layer_overlay!: UIContainer - layer_sheets!: UIContainer - - // Battleground container - arena!: Arena - - // Background image - background!: UIImage | null - - // Targetting mode (null if we're not in this mode) - targetting!: Targetting - - // Ship list - ship_list!: ShipList - - // Action bar - action_bar!: ActionBar - - // Currently hovered ship - ship_hovered!: Ship | null - - // Ship tooltip - ship_tooltip!: ShipTooltip - - // Character sheet - character_sheet!: CharacterSheet - - // Subscription to the battle log - log_processor!: LogProcessor - - // True if player interaction is allowed - interacting!: boolean - - // Tactical mode toggle - toggle_tactical_mode!: Toggle - - // Toggle for the splash screen display - splash = true - - // Init the view, binding it to a specific battle - init(data: { player: Player, battle: Battle }) { - super.init(data); - - this.player = data.player; - this.actual_battle = data.battle; - this.battle = duplicate(data.battle, TK.SpaceTac); - this.ship_hovered = null; - this.background = null; - this.multi = new MultiBattle(); - - this.toggle_tactical_mode = new Toggle( - () => this.arena.setTacticalMode(true), - () => this.arena.setTacticalMode(false) - ); - } - - // Create view graphics - create() { - super.create(); - - var game = this.game; - this.interacting = false; - this.log_processor = new LogProcessor(this); - - let builder = new UIBuilder(this); - - // Add layers - this.layer_background = this.getLayer("background"); - this.layer_arena = this.getLayer("arena"); - this.layer_borders = this.getLayer("borders"); - this.layer_overlay = this.getLayer("overlay"); - this.layer_sheets = this.getLayer("character_sheet"); - - // Background - this.background = builder.in(this.layer_background).image("battle-background"); - - // Add arena (local battlefield map) - this.arena = new Arena(this, this.layer_arena); - this.arena.callbacks_hover.push(bound(this, "cursorHovered")); - this.arena.callbacks_click.push(bound(this, "cursorClicked")); - - // Add UI elements - this.action_bar = new ActionBar(this); - this.action_bar.setPosition(0, this.getHeight() - 132); - this.ship_list = new ShipList(this, this.battle, this.player, this.toggle_tactical_mode, this, - this.layer_borders, this.getWidth() - 112, 0); - this.ship_list.bindToLog(this.log_processor); - this.ship_tooltip = new ShipTooltip(this); - this.character_sheet = new CharacterSheet(this, CharacterSheetMode.DISPLAY); - this.character_sheet.moveToLayer(this.layer_sheets); - - // Targetting info - this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode, this.arena.range_hint); - this.targetting.moveToLayer(this.arena.layer_targetting); - - // BGM - this.audio.startMusic("mechanolith", 0.2); - - // Key mapping - this.inputs.bind("t", "Show tactical view", () => this.ship_list.info_button.toggle()); - this.inputs.bind("Enter", "Validate action", () => this.validationPressed()); - this.inputs.bind(" ", "Validate action", () => this.validationPressed()); - this.inputs.bind("Escape", "Cancel action", () => this.action_bar.actionEnded()); - range(10).forEach(i => this.inputs.bind(`Numpad${i % 10}`, `Action/target ${i}`, () => this.numberPressed(i))); - range(10).forEach(i => this.inputs.bind(`Digit${i % 10}`, `Action/target ${i}`, () => this.numberPressed(i))); - this.inputs.bindCheat("w", "Win current battle", () => { - nn(this.player.getCheats()).win(); - this.log_processor.fastForward(); - }); - this.inputs.bindCheat("x", "Lose current battle", () => { - nn(this.player.getCheats()).lose(); - this.log_processor.fastForward(); - }); - this.inputs.bindCheat("a", "Use AI to play", () => this.playAI()); - - // "Battle" animation, then start processing the log - if (this.battle.ended) { - this.endBattle(); - } else if (this.splash) { - this.showSplash().then(() => { - this.log_processor.start(); - }); - } else { - this.log_processor.start(); - } - - // If we are on a remote session, start the exchange - if (!this.session.primary && this.gameui.session_token) { - // TODO handle errors or timeout - this.multi.setup(this, this.actual_battle, this.gameui.session_token, false); - } - } - - shutdown() { - super.shutdown(); - - this.log_processor.destroy(); - } - - /** - * Make the AI play current ship - * - * If the AI is already playing, do nothing - */ - playAI(): void { - if (this.session.spectator) { - return; - } - - if (this.actual_battle.playAI(this.debug)) { - if (this.interacting) { - this.action_bar.setShip(new Ship()); - } - this.setInteractionEnabled(false); - } - } - - /** - * Apply an action to the actual battle - */ - applyAction(action: BaseAction, target?: Target): boolean { - if (this.session.spectator) { - return false; - } - - let ship = this.actual_battle.playing_ship; - if (ship) { - let ship_action = ship.actions.getById(action.id); - if (ship_action) { - let result = this.actual_battle.applyOneAction(action.id, target); - if (result) { - this.setInteractionEnabled(false); - } - return result; - } else { - console.error("Action not found in available list", action, ship.actions); - return false; - } - } else { - console.error("Action not applied - ship not playing"); - return false; - } - } - - /** - * Display the splash screen at the start of battle - */ - showSplash(): Promise { - let splash = new BattleSplash(this, this.battle.fleets[0], this.battle.fleets[1]); - return splash.start(this.layer_overlay); - } - - /** - * Handle the pressing of a number key - * - * It may first be used to select an action to play, then to select a target - */ - numberPressed(num: number): void { - if (this.interacting) { - if (this.targetting.active) { - let ship = ifirst(this.battle.iships(true), ship => this.battle.getPlayOrder(ship) == num % 10); - if (ship) { - this.targetting.setTarget(Target.newFromShip(ship)); - } - } else { - this.action_bar.keyActionPressed(num - 1); - } - } - } - - /** - * Handle the pression of a validation key (enter or space) - */ - validationPressed(): void { - if (this.targetting.active) { - this.targetting.validate((action, target) => this.applyAction(action, target)); - } else { - this.action_bar.keyActionPressed(-1); - } - } - - /** - * Method called when the arena cursor is hovered - */ - cursorHovered(location: ArenaLocation | null, ship: Ship | null) { - if (this.targetting.active) { - this.targetting.setTargetFromLocation(location); - } else { - if (ship && this.ship_hovered != ship) { - this.cursorOnShip(ship); - } else if (!ship && this.ship_hovered) { - this.cursorOffShip(this.ship_hovered); - } - } - } - - /** - * Method called when cursor starts hovering over a ship (or its icon) - */ - cursorOnShip(ship: Ship): void { - if (ship.alive) { - this.setShipHovered(ship); - } - } - - /** - * Method called when cursor stops hovering over a ship (or its icon) - */ - cursorOffShip(ship: Ship): void { - if (this.ship_hovered === ship) { - this.setShipHovered(null); - } - } - - /** - * Method called when cursor has been clicked (in space or on a ship) - */ - cursorClicked(): void { - if (this.targetting.active) { - this.validationPressed(); - } else if (this.ship_hovered && this.player.is(this.ship_hovered.fleet.player) && this.interacting) { - this.character_sheet.show(this.ship_hovered); - this.setShipHovered(null); - } else { - this.log_processor.fastForward(); - } - } - - /** - * Set the currently hovered ship - */ - setShipHovered(ship: Ship | null): void { - this.ship_hovered = ship; - this.arena.setShipHovered(ship); - this.ship_list.setHovered(ship); - - if (ship) { - this.ship_tooltip.setShip(ship); - } else { - this.ship_tooltip.hide(); - } - } - - // Enable or disable the global player interaction - // Disable interaction when it is the AI turn, or when the current ship can't play - setInteractionEnabled(enabled: boolean): void { - if (this.session.spectator) { - enabled = false; - } - - if (enabled != this.interacting) { - this.action_bar.setInteractivity(enabled); - this.exitTargettingMode(); - this.interacting = enabled; - - if (!enabled) { - this.setShipHovered(null); - } - } - } - - // Enter targetting mode - // While in this mode, the Targetting object will receive hover and click events, and handle them - enterTargettingMode(ship: Ship, action: BaseAction, mode: ActionTargettingMode): Targetting | null { - if (!this.interacting) { - return null; - } - - this.setShipHovered(null); - - this.targetting.setAction(ship, action, mode); - return this.targetting; - } - - // Exit targetting mode - exitTargettingMode(): void { - this.targetting.setAction(null, null); - } - - /** - * End the battle and show the outcome dialog - */ - endBattle() { - let battle = this.actual_battle; - if (battle.outcome) { - this.setInteractionEnabled(false); - - this.session.setBattleEnded(); - - battle.stats.processLog(battle.log, this.player.fleet); - - new OutcomeDialog(this, this.player, battle.outcome, battle.stats); - } else { - console.error("Battle not ended !"); - } - } - - /** - * Exit the battle, and go back to map - */ - exitBattle() { - this.session.exitBattle(); - this.backToRouter(); - } - - /** - * Revert the battle, and go back to map - */ - revertBattle() { - this.session.revertBattle(); - this.backToRouter(); - } - } +/** + * Interface for interacting with a ship (hover and click) + */ +export interface IShipButton { + cursorOnShip: (ship: Ship) => void; + cursorOffShip: (ship: Ship) => void; + cursorClicked: () => void; +} + +/** + * Interactive view of a Battle + */ +export class BattleView extends BaseView implements IShipButton { + // Internal battle state + actual_battle!: Battle + + // Displayed battle state + battle!: Battle + + // Interacting player + player!: Player + + // Multiplayer sharing + multi!: MultiBattle + + // Layers + layer_background!: UIContainer + layer_arena!: UIContainer + layer_borders!: UIContainer + layer_overlay!: UIContainer + layer_sheets!: UIContainer + + // Battleground container + arena!: Arena + + // Background image + background!: UIImage | null + + // Targetting mode (null if we're not in this mode) + targetting!: Targetting + + // Ship list + ship_list!: ShipList + + // Action bar + action_bar!: ActionBar + + // Currently hovered ship + ship_hovered!: Ship | null + + // Ship tooltip + ship_tooltip!: ShipTooltip + + // Character sheet + character_sheet!: CharacterSheet + + // Subscription to the battle log + log_processor!: LogProcessor + + // True if player interaction is allowed + interacting!: boolean + + // Tactical mode toggle + toggle_tactical_mode!: Toggle + + // Toggle for the splash screen display + splash = true + + // Init the view, binding it to a specific battle + init(data: { player: Player, battle: Battle }) { + super.init(data); + + this.player = data.player; + this.actual_battle = data.battle; + this.battle = duplicate(data.battle, NAMESPACE); + this.ship_hovered = null; + this.background = null; + this.multi = new MultiBattle(); + + this.toggle_tactical_mode = new Toggle( + () => this.arena.setTacticalMode(true), + () => this.arena.setTacticalMode(false) + ); + } + + // Create view graphics + create() { + super.create(); + + var game = this.game; + this.interacting = false; + this.log_processor = new LogProcessor(this); + + let builder = new UIBuilder(this); + + // Add layers + this.layer_background = this.getLayer("background"); + this.layer_arena = this.getLayer("arena"); + this.layer_borders = this.getLayer("borders"); + this.layer_overlay = this.getLayer("overlay"); + this.layer_sheets = this.getLayer("character_sheet"); + + // Background + this.background = builder.in(this.layer_background).image("battle-background"); + + // Add arena (local battlefield map) + this.arena = new Arena(this, this.layer_arena); + this.arena.callbacks_hover.push(bound(this, "cursorHovered")); + this.arena.callbacks_click.push(bound(this, "cursorClicked")); + + // Add UI elements + this.action_bar = new ActionBar(this); + this.action_bar.setPosition(0, this.getHeight() - 132); + this.ship_list = new ShipList(this, this.battle, this.player, this.toggle_tactical_mode, this, + this.layer_borders, this.getWidth() - 112, 0); + this.ship_list.bindToLog(this.log_processor); + this.ship_tooltip = new ShipTooltip(this); + this.character_sheet = new CharacterSheet(this, CharacterSheetMode.DISPLAY); + this.character_sheet.moveToLayer(this.layer_sheets); + + // Targetting info + this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode, this.arena.range_hint); + this.targetting.moveToLayer(this.arena.layer_targetting); + + // BGM + this.audio.startMusic("mechanolith", 0.2); + + // Key mapping + this.inputs.bind("t", "Show tactical view", () => this.ship_list.info_button.toggle()); + this.inputs.bind("Enter", "Validate action", () => this.validationPressed()); + this.inputs.bind(" ", "Validate action", () => this.validationPressed()); + this.inputs.bind("Escape", "Cancel action", () => this.action_bar.actionEnded()); + range(10).forEach(i => this.inputs.bind(`Numpad${i % 10}`, `Action/target ${i}`, () => this.numberPressed(i))); + range(10).forEach(i => this.inputs.bind(`Digit${i % 10}`, `Action/target ${i}`, () => this.numberPressed(i))); + this.inputs.bindCheat("w", "Win current battle", () => { + nn(this.player.getCheats()).win(); + this.log_processor.fastForward(); + }); + this.inputs.bindCheat("x", "Lose current battle", () => { + nn(this.player.getCheats()).lose(); + this.log_processor.fastForward(); + }); + this.inputs.bindCheat("a", "Use AI to play", () => this.playAI()); + + // "Battle" animation, then start processing the log + if (this.battle.ended) { + this.endBattle(); + } else if (this.splash) { + this.showSplash().then(() => { + this.log_processor.start(); + }); + } else { + this.log_processor.start(); + } + + // If we are on a remote session, start the exchange + if (!this.session.primary && this.gameui.session_token) { + // TODO handle errors or timeout + this.multi.setup(this, this.actual_battle, this.gameui.session_token, false); + } + } + + shutdown() { + super.shutdown(); + + this.log_processor.destroy(); + } + + /** + * Make the AI play current ship + * + * If the AI is already playing, do nothing + */ + playAI(): void { + if (this.session.spectator) { + return; + } + + if (this.actual_battle.playAI(this.debug)) { + if (this.interacting) { + this.action_bar.setShip(new Ship()); + } + this.setInteractionEnabled(false); + } + } + + /** + * Apply an action to the actual battle + */ + applyAction(action: BaseAction, target?: Target): boolean { + if (this.session.spectator) { + return false; + } + + let ship = this.actual_battle.playing_ship; + if (ship) { + let ship_action = ship.actions.getById(action.id); + if (ship_action) { + let result = this.actual_battle.applyOneAction(action.id, target); + if (result) { + this.setInteractionEnabled(false); + } + return result; + } else { + console.error("Action not found in available list", action, ship.actions); + return false; + } + } else { + console.error("Action not applied - ship not playing"); + return false; + } + } + + /** + * Display the splash screen at the start of battle + */ + showSplash(): Promise { + let splash = new BattleSplash(this, this.battle.fleets[0], this.battle.fleets[1]); + return splash.start(this.layer_overlay); + } + + /** + * Handle the pressing of a number key + * + * It may first be used to select an action to play, then to select a target + */ + numberPressed(num: number): void { + if (this.interacting) { + if (this.targetting.active) { + let ship = ifirst(this.battle.iships(true), ship => this.battle.getPlayOrder(ship) == num % 10); + if (ship) { + this.targetting.setTarget(Target.newFromShip(ship)); + } + } else { + this.action_bar.keyActionPressed(num - 1); + } + } + } + + /** + * Handle the pression of a validation key (enter or space) + */ + validationPressed(): void { + if (this.targetting.active) { + this.targetting.validate((action, target) => this.applyAction(action, target)); + } else { + this.action_bar.keyActionPressed(-1); + } + } + + /** + * Method called when the arena cursor is hovered + */ + cursorHovered(location: ArenaLocation | null, ship: Ship | null) { + if (this.targetting.active) { + this.targetting.setTargetFromLocation(location); + } else { + if (ship && this.ship_hovered != ship) { + this.cursorOnShip(ship); + } else if (!ship && this.ship_hovered) { + this.cursorOffShip(this.ship_hovered); + } + } + } + + /** + * Method called when cursor starts hovering over a ship (or its icon) + */ + cursorOnShip(ship: Ship): void { + if (ship.alive) { + this.setShipHovered(ship); + } + } + + /** + * Method called when cursor stops hovering over a ship (or its icon) + */ + cursorOffShip(ship: Ship): void { + if (this.ship_hovered === ship) { + this.setShipHovered(null); + } + } + + /** + * Method called when cursor has been clicked (in space or on a ship) + */ + cursorClicked(): void { + if (this.targetting.active) { + this.validationPressed(); + } else if (this.ship_hovered && this.player.is(this.ship_hovered.fleet.player) && this.interacting) { + this.character_sheet.show(this.ship_hovered); + this.setShipHovered(null); + } else { + this.log_processor.fastForward(); + } + } + + /** + * Set the currently hovered ship + */ + setShipHovered(ship: Ship | null): void { + this.ship_hovered = ship; + this.arena.setShipHovered(ship); + this.ship_list.setHovered(ship); + + if (ship) { + this.ship_tooltip.setShip(ship); + } else { + this.ship_tooltip.hide(); + } + } + + // Enable or disable the global player interaction + // Disable interaction when it is the AI turn, or when the current ship can't play + setInteractionEnabled(enabled: boolean): void { + if (this.session.spectator) { + enabled = false; + } + + if (enabled != this.interacting) { + this.action_bar.setInteractivity(enabled); + this.exitTargettingMode(); + this.interacting = enabled; + + if (!enabled) { + this.setShipHovered(null); + } + } + } + + // Enter targetting mode + // While in this mode, the Targetting object will receive hover and click events, and handle them + enterTargettingMode(ship: Ship, action: BaseAction, mode: ActionTargettingMode): Targetting | null { + if (!this.interacting) { + return null; + } + + this.setShipHovered(null); + + this.targetting.setAction(ship, action, mode); + return this.targetting; + } + + // Exit targetting mode + exitTargettingMode(): void { + this.targetting.setAction(null, null); + } + + /** + * End the battle and show the outcome dialog + */ + endBattle() { + let battle = this.actual_battle; + if (battle.outcome) { + this.setInteractionEnabled(false); + + this.session.setBattleEnded(); + + battle.stats.processLog(battle.log, this.player.fleet); + + new OutcomeDialog(this, this.player, battle.outcome, battle.stats); + } else { + console.error("Battle not ended !"); + } + } + + /** + * Exit the battle, and go back to map + */ + exitBattle() { + this.session.exitBattle(); + this.backToRouter(); + } + + /** + * Revert the battle, and go back to map + */ + revertBattle() { + this.session.revertBattle(); + this.backToRouter(); + } } diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index 74d30a9..3dd978f 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -1,351 +1,369 @@ -module TK.SpaceTac.UI { - /** - * Processor of diffs coming from the battle log - * - * This will bind to the actual battle log, update the "displayed" battle state accordingly, and refresh the view. - */ - export class LogProcessor { - // Link to the battle view - private view: BattleView +import { Diff } from "../../common/DiffLog" +import { nna } from "../../common/Tools" +import { EndTurnAction } from "../../core/actions/EndTurnAction" +import { TriggerAction } from "../../core/actions/TriggerAction" +import { Battle } from "../../core/Battle" +import { BattleLogClient } from "../../core/BattleLog" +import { BaseBattleDiff, BaseBattleShipDiff } from "../../core/diffs/BaseBattleDiff" +import { EndBattleDiff } from "../../core/diffs/EndBattleDiff" +import { ProjectileFiredDiff } from "../../core/diffs/ProjectileFiredDiff" +import { ShipActionEndedDiff } from "../../core/diffs/ShipActionEndedDiff" +import { ShipChangeDiff } from "../../core/diffs/ShipChangeDiff" +import { ShipDeathDiff } from "../../core/diffs/ShipDeathDiff" +import { PersonalityReactionConversation } from "../../core/PersonalityReactions" +import { Player } from "../../core/Player" +import { Ship } from "../../core/Ship" +import { UIBuilder } from "../common/UIBuilder" +import { UIConversation } from "../common/UIConversation" +import { BattleView } from "./BattleView" +import { WeaponEffect } from "./WeaponEffect" - // Log client (to receive actual battle diffs) - private log: BattleLogClient +/** + * Processor of diffs coming from the battle log + * + * This will bind to the actual battle log, update the "displayed" battle state accordingly, and refresh the view. + */ +export class LogProcessor { + // Link to the battle view + private view: BattleView - // Registered subscribers - private subscriber: LogProcessorSubscriber[] = [] + // Log client (to receive actual battle diffs) + private log: BattleLogClient - // Background delegates promises - private background_promises: Promise[] = [] + // Registered subscribers + private subscriber: LogProcessorSubscriber[] = [] - // Speed control - private speed = 1 - private temp_speed?: number + // Background delegates promises + private background_promises: Promise[] = [] - // Debug indicators - private debug = false - private ai_disabled = false + // Speed control + private speed = 1 + private temp_speed?: number - constructor(view: BattleView) { - this.view = view; - this.log = new BattleLogClient(view.battle, view.actual_battle.log); + // Debug indicators + private debug = false + private ai_disabled = false - view.inputs.bindCheat("PageUp", "Step backward", () => { - this.log.backward(); - }); - view.inputs.bindCheat("PageDown", "Step forward", () => { - this.log.forward(); - }); - view.inputs.bindCheat("Home", "Jump to beginning", () => { - this.log.jumpToStart(); - }); - view.inputs.bindCheat("End", "Jump to end", () => { - this.log.jumpToEnd(); - }); + constructor(view: BattleView) { + this.view = view; + this.log = new BattleLogClient(view.battle, view.actual_battle.log); - // Internal subscribers - this.register((diff) => this.checkReaction(diff)); - this.register((diff) => this.checkControl(diff)); - this.register((diff) => this.checkProjectileFired(diff)); - this.register((diff) => this.checkShipDeath(diff)); - this.register((diff) => this.checkBattleEnded(diff)); + view.inputs.bindCheat("PageUp", "Step backward", () => { + this.log.backward(); + }); + view.inputs.bindCheat("PageDown", "Step forward", () => { + this.log.forward(); + }); + view.inputs.bindCheat("Home", "Jump to beginning", () => { + this.log.jumpToStart(); + }); + view.inputs.bindCheat("End", "Jump to end", () => { + this.log.jumpToEnd(); + }); + + // Internal subscribers + this.register((diff) => this.checkReaction(diff)); + this.register((diff) => this.checkControl(diff)); + this.register((diff) => this.checkProjectileFired(diff)); + this.register((diff) => this.checkShipDeath(diff)); + this.register((diff) => this.checkBattleEnded(diff)); + } + + /** + * Start log processing + */ + start() { + if (!this.view.gameui.isTesting) { + this.log.play(async diff => { + while (this.view.isPaused()) { + await this.view.timer.sleep(500); } - /** - * Start log processing - */ - start() { - if (!this.view.gameui.isTesting) { - this.log.play(async diff => { - while (this.view.isPaused()) { - await this.view.timer.sleep(500); - } + await this.processBattleDiff(diff); - await this.processBattleDiff(diff); - - if (this.log.atEnd()) { - this.temp_speed = undefined; - } - }); - - this.transferControl(); - } + if (this.log.atEnd()) { + this.temp_speed = undefined; } + }); - /** - * Process all pending diffs, synchronously - */ - processPending() { - if (this.log.isPlaying()) { - throw new Error("Cannot process diffs synchronously while playing the log"); - } else { - let diff: Diff | null; - while (diff = this.log.forward()) { - this.processBattleDiff(diff, false); - } - } + this.transferControl(); + } + } + + /** + * Process all pending diffs, synchronously + */ + processPending() { + if (this.log.isPlaying()) { + throw new Error("Cannot process diffs synchronously while playing the log"); + } else { + let diff: Diff | null; + while (diff = this.log.forward()) { + this.processBattleDiff(diff, false); + } + } + } + + /** + * Fast forward to the end of log, then resume normal speed + */ + fastForward(speed = 3): void { + if (!this.log.atEnd()) { + this.temp_speed = speed; + } + } + + /** + * Destroy the processor + * + * This should be done to ensure it will stop processing and free resources + */ + destroy() { + if (this.log.isPlaying()) { + this.log.stop(true); + } + } + + /** + * Check if we need a player or AI to interact at this point + */ + getPlayerNeeded(): Player | null { + if (this.log.isPlaying() && this.log.atEnd()) { + let playing_ship = this.view.actual_battle.playing_ship; + return playing_ship ? playing_ship.getPlayer() : null; + } else { + return null; + } + } + + /** + * Register a diff subscriber + */ + register(subscriber: LogProcessorSubscriber) { + this.subscriber.push(subscriber); + } + + /** + * Register a diff for a specific ship + */ + registerForShip(ship: Ship, subscriber: (diff: BaseBattleShipDiff) => LogProcessorDelegate) { + this.register(diff => { + if (diff instanceof BaseBattleShipDiff && diff.ship_id === ship.id) { + return subscriber(diff); + } else { + return {}; + } + }); + } + + /** + * Register to playing ship changes + * + * If *initial* is true, the callback will be fired once at register time + * + * If *immediate* is true, the ShipChangeDiff is watched, otherwise the end of the EndTurn action + */ + watchForShipChange(callback: (ship: Ship) => LogProcessorDelegate, initial = true, immediate = false) { + this.register(diff => { + let changed = false; + if (immediate && diff instanceof ShipChangeDiff) { + changed = true; + } else if (!immediate && diff instanceof ShipActionEndedDiff) { + let ship = this.view.battle.getShip(diff.ship_id); + if (ship && ship.actions.getById(diff.action) instanceof EndTurnAction) { + changed = true; } + } - /** - * Fast forward to the end of log, then resume normal speed - */ - fastForward(speed = 3): void { - if (!this.log.atEnd()) { - this.temp_speed = speed; - } + if (changed) { + let ship = this.view.battle.playing_ship; + if (ship) { + return callback(ship); + } else { + return {}; } + } else { + return {}; + } + }); - /** - * Destroy the processor - * - * This should be done to ensure it will stop processing and free resources - */ - destroy() { - if (this.log.isPlaying()) { - this.log.stop(true); - } + if (initial) { + let ship = this.view.battle.playing_ship; + if (ship) { + let result = callback(ship); + if (result.foreground) { + let promise = result.foreground(0); + if (result.background) { + let next = result.background; + promise.then(() => next(0)); + } + } else if (result.background) { + result.background(0); } + } + } + } - /** - * Check if we need a player or AI to interact at this point - */ - getPlayerNeeded(): Player | null { - if (this.log.isPlaying() && this.log.atEnd()) { - let playing_ship = this.view.actual_battle.playing_ship; - return playing_ship ? playing_ship.getPlayer() : null; - } else { - return null; - } - } + /** + * Process a single battle diff + */ + async processBattleDiff(diff: BaseBattleDiff, timed = true): Promise { + if (this.debug) { + console.log("Battle diff", diff); + } + let speed = timed ? (typeof this.temp_speed == "undefined" ? this.speed : this.temp_speed) : 0; - /** - * Register a diff subscriber - */ - register(subscriber: LogProcessorSubscriber) { - this.subscriber.push(subscriber); - } + // TODO add priority to sort the delegates + let delegates = this.subscriber.map(subscriber => subscriber(diff)); + let foregrounds = nna(delegates.map(delegate => delegate.foreground || null)); + let backgrounds = nna(delegates.map(delegate => delegate.background || null)); - /** - * Register a diff for a specific ship - */ - registerForShip(ship: Ship, subscriber: (diff: BaseBattleShipDiff) => LogProcessorDelegate) { - this.register(diff => { - if (diff instanceof BaseBattleShipDiff && diff.ship_id === ship.id) { - return subscriber(diff); - } else { - return {}; - } - }); - } + if (foregrounds.length > 0) { + if (this.background_promises.length > 0) { + await Promise.all(this.background_promises); + this.background_promises = []; + } - /** - * Register to playing ship changes - * - * If *initial* is true, the callback will be fired once at register time - * - * If *immediate* is true, the ShipChangeDiff is watched, otherwise the end of the EndTurn action - */ - watchForShipChange(callback: (ship: Ship) => LogProcessorDelegate, initial = true, immediate = false) { - this.register(diff => { - let changed = false; - if (immediate && diff instanceof ShipChangeDiff) { - changed = true; - } else if (!immediate && diff instanceof ShipActionEndedDiff) { - let ship = this.view.battle.getShip(diff.ship_id); - if (ship && ship.actions.getById(diff.action) instanceof EndTurnAction) { - changed = true; - } - } - - if (changed) { - let ship = this.view.battle.playing_ship; - if (ship) { - return callback(ship); - } else { - return {}; - } - } else { - return {}; - } - }); - - if (initial) { - let ship = this.view.battle.playing_ship; - if (ship) { - let result = callback(ship); - if (result.foreground) { - let promise = result.foreground(0); - if (result.background) { - let next = result.background; - promise.then(() => next(0)); - } - } else if (result.background) { - result.background(0); - } - } - } - } - - /** - * Process a single battle diff - */ - async processBattleDiff(diff: BaseBattleDiff, timed = true): Promise { - if (this.debug) { - console.log("Battle diff", diff); - } - let speed = timed ? (typeof this.temp_speed == "undefined" ? this.speed : this.temp_speed) : 0; - - // TODO add priority to sort the delegates - let delegates = this.subscriber.map(subscriber => subscriber(diff)); - let foregrounds = nna(delegates.map(delegate => delegate.foreground || null)); - let backgrounds = nna(delegates.map(delegate => delegate.background || null)); - - if (foregrounds.length > 0) { - if (this.background_promises.length > 0) { - await Promise.all(this.background_promises); - this.background_promises = []; - } - - let promises = foregrounds.map(foreground => foreground(speed)); - await Promise.all(promises); - } - - let promises = backgrounds.map(background => background(speed)); - this.background_promises = this.background_promises.concat(promises); - } - - /** - * Transfer control to the needed player (or not) - */ - private transferControl() { - let player = this.getPlayerNeeded(); - if (player) { - if (player.is(this.view.player)) { - this.view.setInteractionEnabled(true); - } else if (!this.ai_disabled) { - this.view.playAI(); - } else { - this.view.applyAction(EndTurnAction.SINGLETON); - } - } else { - this.view.setInteractionEnabled(false); - } - } - - /** - * Check if a personality reaction should be triggered for a diff - */ - private checkReaction(diff: BaseBattleDiff): LogProcessorDelegate { - if (this.log.isPlaying()) { - let reaction = this.view.session.reactions.check(this.view.player, this.view.battle, this.view.battle.playing_ship, diff); - if (reaction) { - return { - foreground: async () => { - if (reaction instanceof PersonalityReactionConversation) { - let builder = new UIBuilder(this.view, this.view.layer_overlay); - let conversation = UIConversation.newFromPieces(builder, reaction.messages); - await conversation.waitEnd(); - } else { - console.warn("[LogProcessor] Unknown personality reaction type", reaction); - } - } - }; - } - } - - return {}; - } - - /** - * Check if control should be transferred to the player, or an AI, after a diff - */ - private checkControl(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof ShipActionEndedDiff) { - return { - foreground: async () => this.transferControl() - } - } else { - return {}; - } - } - - /** - * Check if a projectile is fired - */ - private checkProjectileFired(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof ProjectileFiredDiff) { - let ship = this.view.battle.getShip(diff.ship_id); - if (ship) { - let action = ship.actions.getById(diff.action); - if (action && action instanceof TriggerAction) { - let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); - return { - foreground: async (speed: number) => { - if (speed) { - await effect.start(speed); - } - } - } - } - } - } - - return {}; - } - - /** - * Check if a ship died - */ - private checkShipDeath(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof ShipDeathDiff) { - let ship = this.view.battle.getShip(diff.ship_id); - - if (ship) { - let dead_ship = ship; - return { - foreground: async (speed: number) => { - if (dead_ship.is(this.view.ship_hovered)) { - this.view.setShipHovered(null); - } - this.view.arena.markAsDead(dead_ship); - this.view.ship_list.refresh(); - if (speed) { - await this.view.timer.sleep(2000 / speed); - } - } - } - } - } - - return {}; - } - - /** - * Check if the battle ended - */ - private checkBattleEnded(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof EndBattleDiff) { - return { - foreground: async () => this.view.endBattle() - } - } - - return {}; - } + let promises = foregrounds.map(foreground => foreground(speed)); + await Promise.all(promises); } - /** - * Effective work done by a subscriber - * - * *foreground* is started when no other delegate (background or foreground) is working - * *background* is started when no other foreground delegate is working or pending - */ - export type LogProcessorDelegate = { - foreground?: (speed: number) => Promise, - background?: (speed: number) => Promise, + let promises = backgrounds.map(background => background(speed)); + this.background_promises = this.background_promises.concat(promises); + } + + /** + * Transfer control to the needed player (or not) + */ + private transferControl() { + let player = this.getPlayerNeeded(); + if (player) { + if (player.is(this.view.player)) { + this.view.setInteractionEnabled(true); + } else if (!this.ai_disabled) { + this.view.playAI(); + } else { + this.view.applyAction(EndTurnAction.SINGLETON); + } + } else { + this.view.setInteractionEnabled(false); + } + } + + /** + * Check if a personality reaction should be triggered for a diff + */ + private checkReaction(diff: BaseBattleDiff): LogProcessorDelegate { + if (this.log.isPlaying()) { + let reaction = this.view.session.reactions.check(this.view.player, this.view.battle, this.view.battle.playing_ship, diff); + if (reaction) { + return { + foreground: async () => { + if (reaction instanceof PersonalityReactionConversation) { + let builder = new UIBuilder(this.view, this.view.layer_overlay); + let conversation = UIConversation.newFromPieces(builder, reaction.messages); + await conversation.waitEnd(); + } else { + console.warn("[LogProcessor] Unknown personality reaction type", reaction); + } + } + }; + } } - /** - * Subscriber to receive diffs from the battle log - */ - type LogProcessorSubscriber = (diff: BaseBattleDiff) => LogProcessorDelegate + return {}; + } + + /** + * Check if control should be transferred to the player, or an AI, after a diff + */ + private checkControl(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof ShipActionEndedDiff) { + return { + foreground: async () => this.transferControl() + } + } else { + return {}; + } + } + + /** + * Check if a projectile is fired + */ + private checkProjectileFired(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof ProjectileFiredDiff) { + let ship = this.view.battle.getShip(diff.ship_id); + if (ship) { + let action = ship.actions.getById(diff.action); + if (action && action instanceof TriggerAction) { + let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); + return { + foreground: async (speed: number) => { + if (speed) { + await effect.start(speed); + } + } + } + } + } + } + + return {}; + } + + /** + * Check if a ship died + */ + private checkShipDeath(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof ShipDeathDiff) { + let ship = this.view.battle.getShip(diff.ship_id); + + if (ship) { + let dead_ship = ship; + return { + foreground: async (speed: number) => { + if (dead_ship.is(this.view.ship_hovered)) { + this.view.setShipHovered(null); + } + this.view.arena.markAsDead(dead_ship); + this.view.ship_list.refresh(); + if (speed) { + await this.view.timer.sleep(2000 / speed); + } + } + } + } + } + + return {}; + } + + /** + * Check if the battle ended + */ + private checkBattleEnded(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof EndBattleDiff) { + return { + foreground: async () => this.view.endBattle() + } + } + + return {}; + } } + +/** + * Effective work done by a subscriber + * + * *foreground* is started when no other delegate (background or foreground) is working + * *background* is started when no other foreground delegate is working or pending + */ +export type LogProcessorDelegate = { + foreground?: (speed: number) => Promise, + background?: (speed: number) => Promise, +} + +/** + * Subscriber to receive diffs from the battle log + */ +type LogProcessorSubscriber = (diff: BaseBattleDiff) => LogProcessorDelegate diff --git a/src/ui/battle/MultiBattle.ts b/src/ui/battle/MultiBattle.ts index 07c2b00..79a1982 100644 --- a/src/ui/battle/MultiBattle.ts +++ b/src/ui/battle/MultiBattle.ts @@ -1,100 +1,106 @@ -module TK.SpaceTac.UI { - /** - * Tool to synchronize two players sharing a battle over network - */ - export class MultiBattle { - // Debug mode - debug = false +import { NAMESPACE } from "../.." +import { Serializer } from "../../common/Serializer" +import { Timer } from "../../common/Timer" +import { Battle } from "../../core/Battle" +import { BaseBattleDiff } from "../../core/diffs/BaseBattleDiff" +import { Exchange } from "../../multi/Exchange" +import { BaseView } from "../BaseView" - // Network exchange of messages - exchange!: Multi.Exchange +/** + * Tool to synchronize two players sharing a battle over network + */ +export class MultiBattle { + // Debug mode + debug = false - // True if this peer is the primary one (the one that invited the other) - primary!: boolean + // Network exchange of messages + exchange!: Exchange - // Battle being played - battle!: Battle + // True if this peer is the primary one (the one that invited the other) + primary!: boolean - // Count of battle log events that were processed - processed!: number + // Battle being played + battle!: Battle - // Serializer to use for actions - serializer!: Serializer + // Count of battle log events that were processed + processed!: number - // Timer for scheduling - timer!: Timer + // Serializer to use for actions + serializer!: Serializer - /** - * Setup the session other a token - */ - async setup(view: BaseView, battle: Battle, token: string, primary: boolean) { - if (this.exchange) { - // TODO close it - } + // Timer for scheduling + timer!: Timer - this.battle = battle; - this.primary = primary; - - this.exchange = new Multi.Exchange(view.getConnection(), token, primary); - await this.exchange.start(); - - this.serializer = new Serializer(TK.SpaceTac); - this.processed = this.battle.log.count(); - this.timer = view.timer; - - // This is voluntarily not waited on, as it is a background task - this.backgroundSync(); - } - - /** - * Background work to maintain the battle state in sync between the two peers - */ - async backgroundSync() { - while (true) { // TODO Close the task on exchange close - if (this.exchange.writing) { - await this.sendDiffs(); - } else { - await this.receiveDiff(); - } - } - } - - /** - * Send one new diff from the battle log - */ - async sendDiffs() { - if (this.processed >= this.battle.log.count()) { - await this.timer.sleep(500); - } else { - let diff = this.battle.log.get(this.processed); - this.processed++; - - if (this.debug) { - console.debug("Send diff to exchange", diff); - } - - let data = this.serializer.serialize(diff); - // TODO "over" should be true if the current ship should be played by remote player - await this.exchange.writeMessage(data, false); - } - } - - /** - * Read and apply one diff from the peer - */ - async receiveDiff() { - let message = await this.exchange.readMessage(); - let received = this.serializer.unserialize(message); - if (received instanceof BaseBattleDiff) { - if (this.debug) { - console.log("Received diff from exchange", received); - } - - this.battle.applyDiffs([received]); - } else { - console.error("Exchange received something that is not a battle diff", received); - } - this.processed = this.battle.log.count(); - } + /** + * Setup the session other a token + */ + async setup(view: BaseView, battle: Battle, token: string, primary: boolean) { + if (this.exchange) { + // TODO close it } + + this.battle = battle; + this.primary = primary; + + this.exchange = new Exchange(view.getConnection(), token, primary); + await this.exchange.start(); + + this.serializer = new Serializer(NAMESPACE); + this.processed = this.battle.log.count(); + this.timer = view.timer; + + // This is voluntarily not waited on, as it is a background task + this.backgroundSync(); + } + + /** + * Background work to maintain the battle state in sync between the two peers + */ + async backgroundSync() { + while (true) { // TODO Close the task on exchange close + if (this.exchange.writing) { + await this.sendDiffs(); + } else { + await this.receiveDiff(); + } + } + } + + /** + * Send one new diff from the battle log + */ + async sendDiffs() { + if (this.processed >= this.battle.log.count()) { + await this.timer.sleep(500); + } else { + let diff = this.battle.log.get(this.processed); + this.processed++; + + if (this.debug) { + console.debug("Send diff to exchange", diff); + } + + let data = this.serializer.serialize(diff); + // TODO "over" should be true if the current ship should be played by remote player + await this.exchange.writeMessage(data, false); + } + } + + /** + * Read and apply one diff from the peer + */ + async receiveDiff() { + let message = await this.exchange.readMessage(); + let received = this.serializer.unserialize(message); + if (received instanceof BaseBattleDiff) { + if (this.debug) { + console.log("Received diff from exchange", received); + } + + this.battle.applyDiffs([received]); + } else { + console.error("Exchange received something that is not a battle diff", received); + } + this.processed = this.battle.log.count(); + } } diff --git a/src/ui/battle/OutcomeDialog.ts b/src/ui/battle/OutcomeDialog.ts index ea822a9..8ed9a8e 100644 --- a/src/ui/battle/OutcomeDialog.ts +++ b/src/ui/battle/OutcomeDialog.ts @@ -1,74 +1,76 @@ -/// +import { BattleOutcome } from "../../core/BattleOutcome"; +import { BattleStats } from "../../core/BattleStats"; +import { Player } from "../../core/Player"; +import { UIDialog } from "../common/UIDialog"; +import { BattleView } from "./BattleView"; -module TK.SpaceTac.UI { - /** - * Dialog to display battle outcome - */ - export class OutcomeDialog extends UIDialog { - battleview: BattleView - player: Player - outcome: BattleOutcome - stats: BattleStats +/** + * Dialog to display battle outcome + */ +export class OutcomeDialog extends UIDialog { + battleview: BattleView + player: Player + outcome: BattleOutcome + stats: BattleStats - constructor(parent: BattleView, player: Player, outcome: BattleOutcome, stats: BattleStats) { - super(parent); + constructor(parent: BattleView, player: Player, outcome: BattleOutcome, stats: BattleStats) { + super(parent); - this.battleview = parent; - this.player = player; - this.outcome = outcome; - this.stats = stats; + this.battleview = parent; + this.player = player; + this.outcome = outcome; + this.stats = stats; - this.refreshContent(); - } + this.refreshContent(); + } - /** - * Shortcut to add a single action button at the bottom of dialog - */ - addActionButton(x: number, text: string, tooltip: string, action: Function) { - let button = this.content.button("common-dialog-textbutton", x, 885, action, tooltip, undefined, { - center: true, - text: text, - text_style: { color: "#d9e0e5" } - }); - } + /** + * Shortcut to add a single action button at the bottom of dialog + */ + addActionButton(x: number, text: string, tooltip: string, action: Function) { + let button = this.content.button("common-dialog-textbutton", x, 885, action, tooltip, undefined, { + center: true, + text: text, + text_style: { color: "#d9e0e5" } + }); + } - /** - * Refresh the whole dialog - */ - refreshContent(): void { - let parent = this.battleview; - let outcome = this.outcome; - let victory = outcome.winner && this.player.fleet.is(outcome.winner); + /** + * Refresh the whole dialog + */ + refreshContent(): void { + let parent = this.battleview; + let outcome = this.outcome; + let victory = outcome.winner && this.player.fleet.is(outcome.winner); - this.content.clear(); + this.content.clear(); - this.content.image(victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat", 747, 180, true); + this.content.image(victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat", 747, 180, true); - this.content.text("You", 815, 320, { color: "#ffffff", size: 20 }); - this.content.text("Enemy", 1015, 320, { color: "#ffffff", size: 20 }); - this.stats.getImportant(10).forEach((stat, index) => { - this.content.text(stat.name, 530, 364 + 40 * index, { color: "#ffffff", size: 20 }); - this.content.text(stat.attacker.toString(), 815, 364 + 40 * index, { color: "#8ba883", size: 20, bold: true }); - this.content.text(stat.defender.toString(), 1015, 364 + 40 * index, { color: "#cd6767", size: 20, bold: true }); - }); + this.content.text("You", 815, 320, { color: "#ffffff", size: 20 }); + this.content.text("Enemy", 1015, 320, { color: "#ffffff", size: 20 }); + this.stats.getImportant(10).forEach((stat, index) => { + this.content.text(stat.name, 530, 364 + 40 * index, { color: "#ffffff", size: 20 }); + this.content.text(stat.attacker.toString(), 815, 364 + 40 * index, { color: "#8ba883", size: 20, bold: true }); + this.content.text(stat.defender.toString(), 1015, 364 + 40 * index, { color: "#cd6767", size: 20, bold: true }); + }); - if (!this.battleview.session.hasUniverse()) { - this.addActionButton(747, "Main menu", "Exit the battle and go back to the main menu", () => { - parent.exitBattle(); - }); - } else if (victory) { - this.addActionButton(747, "Back to map", "Exit the battle and go back to the map", () => { - parent.exitBattle(); - }); - } else { - this.addActionButton(535, "Revert battle", "Go back to where the fleet was before the battle happened", () => { - parent.revertBattle(); - }); + if (!this.battleview.session.hasUniverse()) { + this.addActionButton(747, "Main menu", "Exit the battle and go back to the main menu", () => { + parent.exitBattle(); + }); + } else if (victory) { + this.addActionButton(747, "Back to map", "Exit the battle and go back to the map", () => { + parent.exitBattle(); + }); + } else { + this.addActionButton(535, "Revert battle", "Go back to where the fleet was before the battle happened", () => { + parent.revertBattle(); + }); - this.addActionButton(957, "Main menu", "Quit the game, and go back to main menu", () => { - parent.gameui.quitGame(); - }); - } - } + this.addActionButton(957, "Main menu", "Quit the game, and go back to main menu", () => { + parent.gameui.quitGame(); + }); } + } } diff --git a/src/ui/battle/RangeHint.ts b/src/ui/battle/RangeHint.ts index ab6fa85..cc52aa8 100644 --- a/src/ui/battle/RangeHint.ts +++ b/src/ui/battle/RangeHint.ts @@ -1,77 +1,83 @@ -module TK.SpaceTac.UI { - /** - * Graphical hints for movement and weapon range - */ - export class RangeHint { - // Link to the view - private view: BaseView +import { BaseAction } from "../../core/actions/BaseAction"; +import { MoveAction } from "../../core/actions/MoveAction"; +import { Ship } from "../../core/Ship"; +import { BaseView } from "../BaseView"; +import { UIContainer } from "../common/UIContainer"; +import { UIGraphics } from "../common/UIGraphics"; +import { Arena } from "./Arena"; - // Visual information - private info: UIGraphics +/** + * Graphical hints for movement and weapon range + */ +export class RangeHint { + // Link to the view + private view: BaseView - // Size of the arena - private width: number - private height: number + // Visual information + private info: UIGraphics - constructor(arena: Arena) { - this.view = arena.view; + // Size of the arena + private width: number + private height: number - let boundaries = arena.getBoundaries(); - this.width = boundaries.width; - this.height = boundaries.height; + constructor(arena: Arena) { + this.view = arena.view; - this.info = new UIGraphics(arena.view, "info", false); - } + let boundaries = arena.getBoundaries(); + this.width = boundaries.width; + this.height = boundaries.height; - /** - * Set the layer in which the info will be displayed - */ - setLayer(layer: UIContainer, x = 0, y = 0) { - this.info.setPosition(x, y); - layer.add(this.info); - } + this.info = new UIGraphics(arena.view, "info", false); + } - /** - * Clear displayed information - */ - clear() { - this.info.clear(); - this.info.visible = false; - } + /** + * Set the layer in which the info will be displayed + */ + setLayer(layer: UIContainer, x = 0, y = 0) { + this.info.setPosition(x, y); + layer.add(this.info); + } - /** - * Update displayed information - */ - update(ship: Ship, action: BaseAction, radius = action.getRangeRadius(ship)): void { - let yescolor = 0x000000; - let nocolor = 0x242022; - this.info.clear(); + /** + * Clear displayed information + */ + clear() { + this.info.clear(); + this.info.visible = false; + } - if (radius) { - this.info.fillStyle(nocolor); - this.info.fillRect(0, 0, this.width, this.height); + /** + * Update displayed information + */ + update(ship: Ship, action: BaseAction, radius = action.getRangeRadius(ship)): void { + let yescolor = 0x000000; + let nocolor = 0x242022; + this.info.clear(); - this.info.fillStyle(yescolor); - this.info.fillCircle(ship.arena_x, ship.arena_y, radius); + if (radius) { + this.info.fillStyle(nocolor); + this.info.fillRect(0, 0, this.width, this.height); - if (action instanceof MoveAction) { - let exclusions = action.getExclusionAreas(ship); + this.info.fillStyle(yescolor); + this.info.fillCircle(ship.arena_x, ship.arena_y, radius); - this.info.fillStyle(nocolor); - this.info.fillRect(0, 0, this.width, exclusions.hard_border); - this.info.fillRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border); - this.info.fillRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); - this.info.fillRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); + if (action instanceof MoveAction) { + let exclusions = action.getExclusionAreas(ship); - exclusions.obstacles.forEach(obstacle => { - this.info.fillCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle); - }); - } + this.info.fillStyle(nocolor); + this.info.fillRect(0, 0, this.width, exclusions.hard_border); + this.info.fillRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border); + this.info.fillRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); + this.info.fillRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2); - this.info.visible = true; - } else { - this.info.visible = false; - } - } + exclusions.obstacles.forEach(obstacle => { + this.info.fillCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle); + }); + } + + this.info.visible = true; + } else { + this.info.visible = false; } + } } diff --git a/src/ui/battle/ShipList.spec.ts b/src/ui/battle/ShipList.spec.ts index 386363e..d9fbe60 100644 --- a/src/ui/battle/ShipList.spec.ts +++ b/src/ui/battle/ShipList.spec.ts @@ -1,88 +1,95 @@ -module TK.SpaceTac.UI.Specs { - testing("ShipList", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { Toggle } from "../../common/Toggle"; +import { nn, nop } from "../../common/Tools"; +import { Battle } from "../../core/Battle"; +import { Player } from "../../core/Player"; +import { TestTools } from "../../core/TestTools"; +import { setupEmptyView } from "../TestGame"; +import { ShipList } from "./ShipList"; - function createList(): ShipList { - let view = testgame.view; - let battle = new Battle(); - let player = new Player(); - battle.fleets[0].setPlayer(player); - let tactical_mode = new Toggle(); - let ship_buttons = { - cursorOnShip: nop, - cursorOffShip: nop, - cursorClicked: nop, - }; - let list = new ShipList(view, battle, player, tactical_mode, ship_buttons); - return list; - } +testing("ShipList", test => { + let testgame = setupEmptyView(test); - test.case("handles play position of ships", check => { - let list = createList(); - let battle = list.battle; - check.in("initial", check => { - check.equals(list.items.length, 0, "no item at first"); - }); + function createList(): ShipList { + let view = testgame.view; + let battle = new Battle(); + let player = new Player(); + battle.fleets[0].setPlayer(player); + let tactical_mode = new Toggle(); + let ship_buttons = { + cursorOnShip: nop, + cursorOffShip: nop, + cursorClicked: nop, + }; + let list = new ShipList(view, battle, player, tactical_mode, ship_buttons); + return list; + } - let ship = battle.fleets[0].addShip(); - TestTools.setShipModel(ship, 10, 0); - list.setShipsFromBattle(battle, false); - check.in("one ship added but not in play order", check => { - check.equals(list.items.length, 1, "item count"); - check.equals(list.items[0].visible, false, "ship card not visible"); - }); - - battle.throwInitiative(); - list.refresh(0); - check.in("ship now in play order", check => { - check.equals(list.items[0].visible, true, "ship card visible"); - }); - - ship = battle.fleets[1].addShip(); - TestTools.setShipModel(ship, 10, 0); - battle.throwInitiative(); - list.setShipsFromBattle(battle, false); - check.in("ship added in the other fleet", check => { - check.equals(list.items.length, 2, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 744 }, "second ship position"); - }); - - battle.setPlayingShip(battle.play_order[0]); - list.refresh(0); - check.in("started", check => { - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - }); - - battle.advanceToNextShip(); - list.refresh(0); - check.in("end turn", check => { - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: -14, y: 962 }, "second ship position"); - }); - - ship = battle.fleets[1].addShip(); - TestTools.setShipModel(ship, 10, 0); - battle.throwInitiative(); - battle.setPlayingShip(battle.play_order[0]); - list.setShipsFromBattle(battle, false); - check.in("third ship added", check => { - check.equals(list.items.length, 3, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - check.equals(nn(list.findItem(battle.play_order[2])).location, { x: 2, y: 744 }, "third ship position"); - }); - - let dead = battle.play_order[1]; - dead.setDead(); - list.refresh(0); - check.in("ship dead", check => { - check.equals(list.items.length, 3, "item count"); - check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); - check.equals(nn(list.findItem(dead)).location, { x: 200, y: 843 }, "dead ship position"); - check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); - }); - }); + test.case("handles play position of ships", check => { + let list = createList(); + let battle = list.battle; + check.in("initial", check => { + check.equals(list.items.length, 0, "no item at first"); }); -} + + let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 10, 0); + list.setShipsFromBattle(battle, false); + check.in("one ship added but not in play order", check => { + check.equals(list.items.length, 1, "item count"); + check.equals(list.items[0].visible, false, "ship card not visible"); + }); + + battle.throwInitiative(); + list.refresh(0); + check.in("ship now in play order", check => { + check.equals(list.items[0].visible, true, "ship card visible"); + }); + + ship = battle.fleets[1].addShip(); + TestTools.setShipModel(ship, 10, 0); + battle.throwInitiative(); + list.setShipsFromBattle(battle, false); + check.in("ship added in the other fleet", check => { + check.equals(list.items.length, 2, "item count"); + check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); + check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 744 }, "second ship position"); + }); + + battle.setPlayingShip(battle.play_order[0]); + list.refresh(0); + check.in("started", check => { + check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); + check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); + }); + + battle.advanceToNextShip(); + list.refresh(0); + check.in("end turn", check => { + check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position"); + check.equals(nn(list.findItem(battle.play_order[1])).location, { x: -14, y: 962 }, "second ship position"); + }); + + ship = battle.fleets[1].addShip(); + TestTools.setShipModel(ship, 10, 0); + battle.throwInitiative(); + battle.setPlayingShip(battle.play_order[0]); + list.setShipsFromBattle(battle, false); + check.in("third ship added", check => { + check.equals(list.items.length, 3, "item count"); + check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); + check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); + check.equals(nn(list.findItem(battle.play_order[2])).location, { x: 2, y: 744 }, "third ship position"); + }); + + let dead = battle.play_order[1]; + dead.setDead(); + list.refresh(0); + check.in("ship dead", check => { + check.equals(list.items.length, 3, "item count"); + check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position"); + check.equals(nn(list.findItem(dead)).location, { x: 200, y: 843 }, "dead ship position"); + check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position"); + }); + }); +}); diff --git a/src/ui/battle/ShipList.ts b/src/ui/battle/ShipList.ts index 7fd197b..ea3f369 100644 --- a/src/ui/battle/ShipList.ts +++ b/src/ui/battle/ShipList.ts @@ -1,156 +1,170 @@ -module TK.SpaceTac.UI { - /** - * Side bar with all playing ships, sorted by play order - */ - export class ShipList { - // Link to the parent view - view: BaseView +import { iforeach } from "../../common/Iterators" +import { RObjectId } from "../../common/RObject" +import { Toggle } from "../../common/Toggle" +import { first } from "../../common/Tools" +import { Battle } from "../../core/Battle" +import { ShipDamageDiff } from "../../core/diffs/ShipDamageDiff" +import { Player } from "../../core/Player" +import { Ship } from "../../core/Ship" +import { BaseView } from "../BaseView" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { IShipButton } from "./BattleView" +import { LogProcessor } from "./LogProcessor" +import { ShipListItem } from "./ShipListItem" - // Current battle - battle: Battle +/** + * Side bar with all playing ships, sorted by play order + */ +export class ShipList { + // Link to the parent view + view: BaseView - // Current player - player: Player + // Current battle + battle: Battle - // Interface for acting as ship button - ship_buttons: IShipButton + // Current player + player: Player - // Container - container: UIContainer + // Interface for acting as ship button + ship_buttons: IShipButton - // List of ship items - items: ShipListItem[] + // Container + container: UIContainer - // Hovered ship - hovered: ShipListItem | null + // List of ship items + items: ShipListItem[] - // Info button - info_button: UIButton + // Hovered ship + hovered: ShipListItem | null - constructor(view: BaseView, battle: Battle, player: Player, tactical_mode: Toggle, ship_buttons: IShipButton, parent?: UIContainer, x = 0, y = 0) { - let builder = new UIBuilder(view, parent); - this.container = builder.container("shiplist", x, y); + // Info button + info_button: UIButton - builder = builder.in(this.container); - let bg = builder.image("battle-shiplist-background", 0, 0); - bg.setInteractive(); + constructor(view: BaseView, battle: Battle, player: Player, tactical_mode: Toggle, ship_buttons: IShipButton, parent?: UIContainer, x = 0, y = 0) { + let builder = new UIBuilder(view, parent); + this.container = builder.container("shiplist", x, y); - this.view = view; - this.battle = battle; - this.player = player; - this.ship_buttons = ship_buttons; + builder = builder.in(this.container); + let bg = builder.image("battle-shiplist-background", 0, 0); + bg.setInteractive(); - this.items = []; - this.hovered = null; + this.view = view; + this.battle = battle; + this.player = player; + this.ship_buttons = ship_buttons; - this.info_button = builder.button("battle-shiplist-info-button", 0, 0, undefined, "Tactical display", on => tactical_mode.manipulate("shiplist")(on)); + this.items = []; + this.hovered = null; - this.setShipsFromBattle(battle); + this.info_button = builder.button("battle-shiplist-info-button", 0, 0, undefined, "Tactical display", on => tactical_mode.manipulate("shiplist")(on)); + + this.setShipsFromBattle(battle); + } + + /** + * Clear all ship cards + */ + clearAll(): void { + this.items.forEach(ship => ship.destroy()); + this.items = []; + } + + /** + * Rebuild the ship list from an ongoing battle + */ + setShipsFromBattle(battle: Battle, animate = true): void { + this.clearAll(); + iforeach(battle.iships(true), ship => this.addShip(ship)); + this.refresh(animate ? 1 : 0); + } + + /** + * Bind to a log processor, to watch for events + */ + bindToLog(log: LogProcessor): void { + log.watchForShipChange(ship => { + return { + foreground: async (speed: number) => { + this.refresh(speed); } + } + }); - /** - * Clear all ship cards - */ - clearAll(): void { - this.items.forEach(ship => ship.destroy()); - this.items = []; - } - - /** - * Rebuild the ship list from an ongoing battle - */ - setShipsFromBattle(battle: Battle, animate = true): void { - this.clearAll(); - iforeach(battle.iships(true), ship => this.addShip(ship)); - this.refresh(animate ? 1 : 0); - } - - /** - * Bind to a log processor, to watch for events - */ - bindToLog(log: LogProcessor): void { - log.watchForShipChange(ship => { - return { - foreground: async (speed: number) => { - this.refresh(speed); - } - } - }); - - log.register(diff => { - if (diff instanceof ShipDamageDiff) { - return { - background: async () => { - let item = this.findItem(diff.ship_id); - if (item) { - item.setDamageHit(); - } - } - } - } else { - return {}; - } - }) - } - - /** - * Add a ship card - */ - addShip(ship: Ship): ShipListItem { - var owned = ship.isPlayedBy(this.player); - var result = new ShipListItem(this, 200, this.container.height / 2, ship, owned, this.ship_buttons); - this.items.push(result); - this.container.add(result); - return result; - } - - /** - * Find the item (card) that displays a given ship - */ - findItem(ship: Ship | RObjectId | null): ShipListItem | null { - return first(this.items, item => item.ship.is(ship)); - } - - /** - * Update the locations of all items - */ - refresh(speed = 1): void { - let duration = speed ? (1000 / speed) : 0; - this.items.forEach(item => { - if (item.ship.alive) { - let position = this.battle.getPlayOrder(item.ship); - if (position < 0) { - item.visible = false; - } else { - if (position == 0) { - item.moveAt(-14, 962, duration); - } else { - item.moveAt(2, 942 - position * 99, duration); - } - item.visible = true; - item.setZ(99 - position); - } - } else { - item.setZ(100); - item.moveAt(200, item.y, duration); - } - }); - } - - /** - * Set the currently hovered ship - */ - setHovered(ship: Ship | null): void { - if (this.hovered) { - this.hovered.setHovered(false); - this.hovered = null; - } - if (ship) { - this.hovered = this.findItem(ship); - if (this.hovered) { - this.hovered.setHovered(true); - } + log.register(diff => { + if (diff instanceof ShipDamageDiff) { + return { + background: async () => { + let item = this.findItem(diff.ship_id); + if (item) { + item.setDamageHit(); } + } } + } else { + return {}; + } + }) + } + + /** + * Add a ship card + */ + addShip(ship: Ship): ShipListItem { + var owned = ship.isPlayedBy(this.player); + var result = new ShipListItem(this, 200, this.container.height / 2, ship, owned, this.ship_buttons); + this.items.push(result); + this.container.add(result); + return result; + } + + /** + * Find the item (card) that displays a given ship + */ + findItem(ship: Ship | RObjectId | null): ShipListItem | null { + return first(this.items, item => item.ship.is(ship)); + } + + /** + * Update the locations of all items + */ + refresh(speed = 1): void { + let duration = speed ? (1000 / speed) : 0; + this.items.forEach(item => { + if (item.ship.alive) { + let position = this.battle.getPlayOrder(item.ship); + if (position < 0) { + item.visible = false; + } else { + if (position == 0) { + item.moveAt(-14, 962, duration); + } else { + item.moveAt(2, 942 - position * 99, duration); + } + item.visible = true; + item.setZ(99 - position); + } + } else { + item.setZ(100); + item.moveAt(200, item.y, duration); + } + }); + } + + /** + * Set the currently hovered ship + */ + setHovered(ship: Ship | null): void { + if (this.hovered) { + this.hovered.setHovered(false); + this.hovered = null; } + if (ship) { + this.hovered = this.findItem(ship); + if (this.hovered) { + this.hovered.setHovered(true); + } + } + } } diff --git a/src/ui/battle/ShipListItem.ts b/src/ui/battle/ShipListItem.ts index 69bb352..6ab2dea 100644 --- a/src/ui/battle/ShipListItem.ts +++ b/src/ui/battle/ShipListItem.ts @@ -1,91 +1,97 @@ -module TK.SpaceTac.UI { - /** - * One item in a ship list (used in BattleView) - */ - export class ShipListItem extends UIContainer { - // Reference to the view - view: BaseView +import { Ship } from "../../core/Ship" +import { BaseView } from "../BaseView" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { IShipButton } from "./BattleView" +import { ShipList } from "./ShipList" - // Reference to the ship game object - ship: Ship +/** + * One item in a ship list (used in BattleView) + */ +export class ShipListItem extends UIContainer { + // Reference to the view + view: BaseView - // Player indicator - player_indicator: UIImage + // Reference to the ship game object + ship: Ship - // Portrait - portrait: UIImage + // Player indicator + player_indicator: UIImage - // Damage flashing indicator - damage_indicator: UIImage + // Portrait + portrait: UIImage - // Hover indicator - hover_indicator: UIImage + // Damage flashing indicator + damage_indicator: UIImage - // Create a ship button for the battle ship list - constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean, ship_buttons: IShipButton) { - // TODO Make it an UIButton - super(list.view, x, y); - this.view = list.view; - this.ship = ship; + // Hover indicator + hover_indicator: UIImage - let builder = new UIBuilder(list.view, this); + // Create a ship button for the battle ship list + constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean, ship_buttons: IShipButton) { + // TODO Make it an UIButton + super(list.view, x, y); + this.view = list.view; + this.ship = ship; - builder.image("battle-shiplist-item-background"); + let builder = new UIBuilder(list.view, this); - this.player_indicator = builder.image(owned ? "battle-hud-ship-own-mini" : "battle-hud-ship-enemy-mini", 102, 52, true); - this.player_indicator.setAngle(-90); + builder.image("battle-shiplist-item-background"); - this.portrait = builder.image(`ship-${ship.model.code}-sprite`, 52, 52, true); - this.portrait.setScale(0.8) - this.portrait.setAngle(180); + this.player_indicator = builder.image(owned ? "battle-hud-ship-own-mini" : "battle-hud-ship-enemy-mini", 102, 52, true); + this.player_indicator.setAngle(-90); - this.damage_indicator = builder.image("battle-shiplist-damage", 8, 9); - this.damage_indicator.visible = false; + this.portrait = builder.image(`ship-${ship.model.code}-sprite`, 52, 52, true); + this.portrait.setScale(0.8) + this.portrait.setAngle(180); - this.hover_indicator = builder.image("battle-shiplist-hover", 7, 8); - this.hover_indicator.visible = false; + this.damage_indicator = builder.image("battle-shiplist-damage", 8, 9); + this.damage_indicator.visible = false; - this.view.inputs.setHoverClick(this, - () => ship_buttons.cursorOnShip(ship), - () => ship_buttons.cursorOffShip(ship), - () => ship_buttons.cursorClicked() - ); - } + this.hover_indicator = builder.image("battle-shiplist-hover", 7, 8); + this.hover_indicator.visible = false; - get location(): { x: number, y: number } { - return { x: this.x, y: this.y }; - } + this.view.inputs.setHoverClick(this, + () => ship_buttons.cursorOnShip(ship), + () => ship_buttons.cursorOffShip(ship), + () => ship_buttons.cursorClicked() + ); + } - /** - * Flash a damage indicator - */ - setDamageHit() { - this.view.tweens.add({ - targets: this.damage_indicator, - duration: 100, - alpha: 1, - repeat: 2, - yoyo: true - }); - } + get location(): { x: number, y: number } { + return { x: this.x, y: this.y }; + } - /** - * Move to a given location on screen - */ - moveAt(x: number, y: number, duration: number) { - if (duration && (this.x != x || this.y != y)) { - this.view.animations.addAnimation(this, { x: x, y: y }, duration); - } else { - this.x = x; - this.y = y; - } - } + /** + * Flash a damage indicator + */ + setDamageHit() { + this.view.tweens.add({ + targets: this.damage_indicator, + duration: 100, + alpha: 1, + repeat: 2, + yoyo: true + }); + } - /** - * Set the hovered status - */ - setHovered(hovered: boolean) { - this.view.animations.setVisible(this.hover_indicator, hovered, 200); - } + /** + * Move to a given location on screen + */ + moveAt(x: number, y: number, duration: number) { + if (duration && (this.x != x || this.y != y)) { + this.view.animations.addAnimation(this, { x: x, y: y }, duration); + } else { + this.x = x; + this.y = y; } + } + + /** + * Set the hovered status + */ + setHovered(hovered: boolean) { + this.view.animations.setVisible(this.hover_indicator, hovered, 200); + } } diff --git a/src/ui/battle/ShipTooltip.spec.ts b/src/ui/battle/ShipTooltip.spec.ts index 532cf65..c6a510a 100644 --- a/src/ui/battle/ShipTooltip.spec.ts +++ b/src/ui/battle/ShipTooltip.spec.ts @@ -1,33 +1,40 @@ -module TK.SpaceTac.UI.Specs { - testing("ShipTooltip", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { AttributeEffect } from "../../core/effects/AttributeEffect"; +import { AttributeLimitEffect } from "../../core/effects/AttributeLimitEffect"; +import { StickyEffect } from "../../core/effects/StickyEffect"; +import { ShipModel } from "../../core/models/ShipModel"; +import { TestTools } from "../../core/TestTools"; +import { collectImages, collectTexts, setupBattleview } from "../TestGame"; +import { ShipTooltip } from "./ShipTooltip"; - test.case("fills ship details", check => { - let tooltip = new ShipTooltip(testgame.view); - let ship = testgame.view.battle.play_order[2]; - TestTools.setShipModel(ship, 58, 140, 12); - ship.name = "Fury"; - ship.model = new ShipModel("fake", "Fury"); - check.patch(ship.model, "getDescription", () => "Super ship model !"); - TestTools.addWeapon(ship, 50); - TestTools.setAttribute(ship, "evasion", 7); - ship.setValue("hull", 57); - ship.setValue("shield", 100); - ship.setValue("power", 9); - ship.active_effects.add(new AttributeEffect("hull_capacity", 50)); - ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("shield_capacity", 2), 3)); - tooltip.setShip(ship); +testing("ShipTooltip", test => { + let testgame = setupBattleview(test); - let images = collectImages((tooltip).container); - let texts = collectTexts((tooltip).container); - check.contains(images, "ship-fake-portrait"); - check.contains(images, "action-weapon"); - check.equals(texts, [ - "Level 1 Fury", "Plays in 2 turns", - "57", "max", "58", "100", "max", "140", "7", "9", "max", "12", - "Weapon", "• hull capacity +50", "• limit shield capacity to 2 for 3 turns", - "Super ship model !" - ]); - }); - }); -} + test.case("fills ship details", check => { + let tooltip = new ShipTooltip(testgame.view); + let ship = testgame.view.battle.play_order[2]; + TestTools.setShipModel(ship, 58, 140, 12); + ship.name = "Fury"; + ship.model = new ShipModel("fake", "Fury"); + check.patch(ship.model, "getDescription", () => "Super ship model !"); + TestTools.addWeapon(ship, 50); + TestTools.setAttribute(ship, "evasion", 7); + ship.setValue("hull", 57); + ship.setValue("shield", 100); + ship.setValue("power", 9); + ship.active_effects.add(new AttributeEffect("hull_capacity", 50)); + ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("shield_capacity", 2), 3)); + tooltip.setShip(ship); + + let images = collectImages((tooltip).container); + let texts = collectTexts((tooltip).container); + check.contains(images, "ship-fake-portrait"); + check.contains(images, "action-weapon"); + check.equals(texts, [ + "Level 1 Fury", "Plays in 2 turns", + "57", "max", "58", "100", "max", "140", "7", "9", "max", "12", + "Weapon", "• hull capacity +50", "• limit shield capacity to 2 for 3 turns", + "Super ship model !" + ]); + }); +}); diff --git a/src/ui/battle/ShipTooltip.ts b/src/ui/battle/ShipTooltip.ts index c20a468..df32e98 100644 --- a/src/ui/battle/ShipTooltip.ts +++ b/src/ui/battle/ShipTooltip.ts @@ -1,95 +1,101 @@ -/// +import { EndTurnAction } from "../../core/actions/EndTurnAction"; +import { MoveAction } from "../../core/actions/MoveAction"; +import { Battle } from "../../core/Battle"; +import { Player } from "../../core/Player"; +import { Ship } from "../../core/Ship"; +import { Tooltip, TooltipBuilder } from "../common/Tooltip"; +import { UIBuilder } from "../common/UIBuilder"; +import { UITools } from "../common/UITools"; +import { BattleView } from "./BattleView"; -module TK.SpaceTac.UI { - /** - * Tooltip to display ship information on hover - */ - export class ShipTooltip extends Tooltip { - battleview: BattleView +/** + * Tooltip to display ship information on hover + */ +export class ShipTooltip extends Tooltip { + battleview: BattleView - constructor(parent: BattleView) { - super(parent); + constructor(parent: BattleView) { + super(parent); - this.battleview = parent; - } + this.battleview = parent; + } - /** - * Set the current ship to display - */ - setShip(ship: Ship): void { - this.hide(); + /** + * Set the current ship to display + */ + setShip(ship: Ship): void { + this.hide(); - let builder = this.getBuilder(); + let builder = this.getBuilder(); - builder.configure(10, 6, this.battleview.arena.getBoundaries(true)); + builder.configure(10, 6, this.battleview.arena.getBoundaries(true)); - ShipTooltip.fillInfo(builder, ship, this.battleview.battle, this.battleview.player); + ShipTooltip.fillInfo(builder, ship, this.battleview.battle, this.battleview.player); - let sprite = this.battleview.arena.findShipSprite(ship); - if (sprite) { - this.container.show(UITools.getBounds(sprite.frame_owner)); - } - } - - static fillInfo(builder: TooltipBuilder, ship: Ship, battle?: Battle, player?: Player): boolean { - let portrait_bg = builder.image("battle-tooltip-ship-portrait", 0, 0); - builder.in(portrait_bg, builder => { - let portrait = builder.image(`ship-${ship.model.code}-portrait`, 1, 1); - portrait.setScale(0.75); - }); - - let enemy = player && !player.is(ship.fleet.player); - builder.text(ship.getName(), 230, 0, { color: enemy ? "#cc0d00" : "#ffffff", size: 22, bold: true }); - - if (ship.alive) { - if (battle) { - let turns = battle.getPlayOrder(ship); - builder.text((turns == 0) ? "Playing" : ((turns == 1) ? "Plays next" : `Plays in ${turns} turns`), 230, 36, { color: "#cccccc", size: 18 }); - } - - ShipTooltip.addValue(builder, 0, "#eb4e4a", "attribute-hull_capacity", ship.getValue("hull"), ship.getAttribute("hull_capacity")); - ShipTooltip.addValue(builder, 1, "#2ad8dc", "attribute-shield_capacity", ship.getValue("shield"), ship.getAttribute("shield_capacity")); - ShipTooltip.addValue(builder, 2, "#c1f06b", "attribute-evasion", ship.getAttribute("evasion")); - ShipTooltip.addValue(builder, 3, "#ffdd4b", "attribute-power_capacity", ship.getValue("power"), ship.getAttribute("power_capacity")); - - let iy = 210; - - ship.actions.listAll().forEach(action => { - if (!(action instanceof EndTurnAction) && !(action instanceof MoveAction)) { - let icon = builder.image(`action-${action.code}`, 0, iy); - icon.setScale(0.15); - builder.text(action.name, 46, iy + 8); - iy += 40; - } - }); - - ship.active_effects.list().forEach(effect => { - if (!effect.isInternal()) { - builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" }); - iy += 32; - } - }); - - builder.text(ship.model.getDescription(), 0, iy + 4, { size: 14, color: "#999999", width: 540 }); - } else { - builder.text("Emergency Stasis Protocol\nship disabled", 230, 36, - { color: "#a899db", size: 20, center: false, vcenter: false }); - } - - return true; - } - - private static addValue(builder: UIBuilder, idx: number, color: string, icon: string, val: number, max?: number) { - let bg = builder.image("battle-tooltip-ship-value", 252 + idx * 68, 116, true); - - builder.in(bg).styled({ color: color, size: 18, center: true, vcenter: true, bold: true }, builder => { - builder.image(icon, 0, -14, true); - builder.text(`${val}`, 0, 28); - if (max) { - builder.text("max", 0, 58, { size: 10 }); - builder.text(`${max}`, 0, 72, { size: 10 }); - } - }); - } + let sprite = this.battleview.arena.findShipSprite(ship); + if (sprite) { + this.container.show(UITools.getBounds(sprite.frame_owner)); } + } + + static fillInfo(builder: TooltipBuilder, ship: Ship, battle?: Battle, player?: Player): boolean { + let portrait_bg = builder.image("battle-tooltip-ship-portrait", 0, 0); + builder.in(portrait_bg, builder => { + let portrait = builder.image(`ship-${ship.model.code}-portrait`, 1, 1); + portrait.setScale(0.75); + }); + + let enemy = player && !player.is(ship.fleet.player); + builder.text(ship.getName(), 230, 0, { color: enemy ? "#cc0d00" : "#ffffff", size: 22, bold: true }); + + if (ship.alive) { + if (battle) { + let turns = battle.getPlayOrder(ship); + builder.text((turns == 0) ? "Playing" : ((turns == 1) ? "Plays next" : `Plays in ${turns} turns`), 230, 36, { color: "#cccccc", size: 18 }); + } + + ShipTooltip.addValue(builder, 0, "#eb4e4a", "attribute-hull_capacity", ship.getValue("hull"), ship.getAttribute("hull_capacity")); + ShipTooltip.addValue(builder, 1, "#2ad8dc", "attribute-shield_capacity", ship.getValue("shield"), ship.getAttribute("shield_capacity")); + ShipTooltip.addValue(builder, 2, "#c1f06b", "attribute-evasion", ship.getAttribute("evasion")); + ShipTooltip.addValue(builder, 3, "#ffdd4b", "attribute-power_capacity", ship.getValue("power"), ship.getAttribute("power_capacity")); + + let iy = 210; + + ship.actions.listAll().forEach(action => { + if (!(action instanceof EndTurnAction) && !(action instanceof MoveAction)) { + let icon = builder.image(`action-${action.code}`, 0, iy); + icon.setScale(0.15); + builder.text(action.name, 46, iy + 8); + iy += 40; + } + }); + + ship.active_effects.list().forEach(effect => { + if (!effect.isInternal()) { + builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" }); + iy += 32; + } + }); + + builder.text(ship.model.getDescription(), 0, iy + 4, { size: 14, color: "#999999", width: 540 }); + } else { + builder.text("Emergency Stasis Protocol\nship disabled", 230, 36, + { color: "#a899db", size: 20, center: false, vcenter: false }); + } + + return true; + } + + private static addValue(builder: UIBuilder, idx: number, color: string, icon: string, val: number, max?: number) { + let bg = builder.image("battle-tooltip-ship-value", 252 + idx * 68, 116, true); + + builder.in(bg).styled({ color: color, size: 18, center: true, vcenter: true, bold: true }, builder => { + builder.image(icon, 0, -14, true); + builder.text(`${val}`, 0, 28); + if (max) { + builder.text("max", 0, 58, { size: 10 }); + builder.text(`${max}`, 0, 72, { size: 10 }); + } + }); + } } diff --git a/src/ui/battle/Targetting.spec.ts b/src/ui/battle/Targetting.spec.ts index 9d08b12..f4c8218 100644 --- a/src/ui/battle/Targetting.spec.ts +++ b/src/ui/battle/Targetting.spec.ts @@ -1,188 +1,197 @@ -module TK.SpaceTac.UI.Specs { - testing("Targetting", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { iterator, nn, nnf } from "../../common/Tools"; +import { ActionTargettingMode, BaseAction } from "../../core/actions/BaseAction"; +import { TriggerAction } from "../../core/actions/TriggerAction"; +import { MoveFireResult } from "../../core/MoveFireSimulator"; +import { Ship } from "../../core/Ship"; +import { Target } from "../../core/Target"; +import { TestTools } from "../../core/TestTools"; +import { setupBattleview } from "../TestGame"; +import { Targetting } from "./Targetting"; - function newTargetting(): Targetting { - return new Targetting(testgame.view, - testgame.view.action_bar, - testgame.view.toggle_tactical_mode, - testgame.view.arena.range_hint); - } +testing("Targetting", test => { + let testgame = setupBattleview(test); - test.case("draws simulation parts", check => { - let targetting = newTargetting(); + function newTargetting(): Targetting { + return new Targetting(testgame.view, + testgame.view.action_bar, + testgame.view.toggle_tactical_mode, + testgame.view.arena.range_hint); + } - let ship = nn(testgame.view.battle.playing_ship); - ship.setArenaPosition(10, 20); - let weapon = TestTools.addWeapon(ship); - let engine = TestTools.addEngine(ship, 12); - targetting.setAction(ship, weapon); + test.case("draws simulation parts", check => { + let targetting = newTargetting(); - let drawvector = check.patch(targetting, "drawVector", null); + let ship = nn(testgame.view.battle.playing_ship); + ship.setArenaPosition(10, 20); + let weapon = TestTools.addWeapon(ship); + let engine = TestTools.addEngine(ship, 12); + targetting.setAction(ship, weapon); - let part = { - action: weapon, - target: new Target(50, 30), - ap: 5, - possible: true - }; - targetting.drawPart(part, true, null); - check.called(drawvector, [ - [0xdc6441, 10, 20, 50, 30, 0] - ]); + let drawvector = check.patch(targetting, "drawVector", null); - targetting.drawPart(part, false, null); - check.called(drawvector, [ - [0x8e8e8e, 10, 20, 50, 30, 0] - ]); + let part = { + action: weapon, + target: new Target(50, 30), + ap: 5, + possible: true + }; + targetting.drawPart(part, true, null); + check.called(drawvector, [ + [0xdc6441, 10, 20, 50, 30, 0] + ]); - targetting.action = engine; - part.action = engine; - targetting.drawPart(part, true, null); - check.called(drawvector, [ - [0xe09c47, 10, 20, 50, 30, 12] - ]); - }) + targetting.drawPart(part, false, null); + check.called(drawvector, [ + [0x8e8e8e, 10, 20, 50, 30, 0] + ]); - test.case("updates impact indicators on ships inside the blast radius", check => { - let targetting = newTargetting(); - let ship = nn(testgame.view.battle.playing_ship); - let impacts = targetting.impact_indicators; - let action = new TriggerAction("weapon", { range: 50 }); + targetting.action = engine; + part.action = engine; + targetting.drawPart(part, true, null); + check.called(drawvector, [ + [0xe09c47, 10, 20, 50, 30, 12] + ]); + }) - let collect = check.patch(action, "getImpactedShips", nnf([], iterator([ - [new Ship(), new Ship(), new Ship()], - [new Ship(), new Ship()], - [] - ]))); - targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 10)); + test.case("updates impact indicators on ships inside the blast radius", check => { + let targetting = newTargetting(); + let ship = nn(testgame.view.battle.playing_ship); + let impacts = targetting.impact_indicators; + let action = new TriggerAction("weapon", { range: 50 }); - check.called(collect, [ - [ship, new Target(20, 10), ship.location] - ]) - check.equals(targetting.impact_indicators.length, 3); - check.equals(targetting.impact_indicators.visible, true); + let collect = check.patch(action, "getImpactedShips", nnf([], iterator([ + [new Ship(), new Ship(), new Ship()], + [new Ship(), new Ship()], + [] + ]))); + targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 10)); - targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 11)); + check.called(collect, [ + [ship, new Target(20, 10), ship.location] + ]) + check.equals(targetting.impact_indicators.length, 3); + check.equals(targetting.impact_indicators.visible, true); - check.called(collect, [ - [ship, new Target(20, 11), ship.location] - ]) - check.equals(targetting.impact_indicators.length, 2); - check.equals(targetting.impact_indicators.visible, true); + targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 11)); - targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 12)); + check.called(collect, [ + [ship, new Target(20, 11), ship.location] + ]) + check.equals(targetting.impact_indicators.length, 2); + check.equals(targetting.impact_indicators.visible, true); - check.called(collect, [ - [ship, new Target(20, 12), ship.location] - ]) - check.equals(targetting.impact_indicators.visible, false); - }) + targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 12)); - test.case("updates graphics from simulation", check => { - let targetting = newTargetting(); - let ship = nn(testgame.view.battle.playing_ship); + check.called(collect, [ + [ship, new Target(20, 12), ship.location] + ]) + check.equals(targetting.impact_indicators.visible, false); + }) - let engine = TestTools.addEngine(ship, 8000); - let weapon = TestTools.addWeapon(ship, 30, 5, 100, 50); - targetting.setAction(ship, weapon); - targetting.setTarget(Target.newFromLocation(156, 65)); + test.case("updates graphics from simulation", check => { + let targetting = newTargetting(); + let ship = nn(testgame.view.battle.playing_ship); - check.patch(targetting, "simulate", () => { - let result = new MoveFireResult(); - result.success = true; - result.complete = true; - result.need_move = true; - result.move_location = Target.newFromLocation(80, 20); - result.can_move = true; - result.can_end_move = true; - result.need_fire = true; - result.can_fire = true; - result.parts = [ - { action: engine, target: Target.newFromLocation(80, 20), ap: 1, possible: true }, - { action: weapon, target: Target.newFromLocation(156, 65), ap: 5, possible: true } - ] - targetting.simulation = result; - }); - targetting.update(); + let engine = TestTools.addEngine(ship, 8000); + let weapon = TestTools.addWeapon(ship, 30, 5, 100, 50); + targetting.setAction(ship, weapon); + targetting.setTarget(Target.newFromLocation(156, 65)); - check.equals(targetting.container.visible, true); - check.equals(targetting.drawn_info.visible, true); - check.equals(targetting.fire_arrow.visible, true); - check.equals(targetting.fire_arrow.x, 156); - check.equals(targetting.fire_arrow.y, 65); - check.nears(targetting.fire_arrow.rotation, 0.534594, 5); - check.equals(targetting.impact_area.visible, true); - check.equals(targetting.impact_area.x, 156); - check.equals(targetting.impact_area.y, 65); - check.equals(targetting.move_ghost.visible, true); - check.equals(targetting.move_ghost.x, 80); - check.equals(targetting.move_ghost.y, 20); - check.nears(targetting.move_ghost.rotation, 0.534594, 5); - }) - - test.case("snaps on ships according to targetting mode", check => { - let targetting = newTargetting(); - let playing_ship = nn(testgame.view.battle.playing_ship); - let action = TestTools.addWeapon(playing_ship); - - let ship1 = testgame.view.battle.play_order[1]; - let ship2 = testgame.view.battle.play_order[2]; - ship1.setArenaPosition(8000, 50); - ship2.setArenaPosition(8000, 230); - - targetting.setAction(playing_ship, action, ActionTargettingMode.SPACE); - targetting.setTargetFromLocation({ x: 8000, y: 60 }); - check.equals(targetting.target, Target.newFromLocation(8000, 60), "space"); - - targetting.setAction(playing_ship, action, ActionTargettingMode.SHIP); - targetting.setTargetFromLocation({ x: 8000, y: 60 }); - check.equals(targetting.target, Target.newFromShip(ship1), "ship 1"); - targetting.setTargetFromLocation({ x: 8100, y: 200 }); - check.equals(targetting.target, Target.newFromShip(ship2), "ship 2"); - - targetting.setAction(playing_ship, action, ActionTargettingMode.SURROUNDINGS); - targetting.setTargetFromLocation({ x: 8000, y: 60 }); - check.equals(targetting.target, new Target(8000, 60, playing_ship), "surroundings 1"); - targetting.setTargetFromLocation({ x: playing_ship.arena_x + 10, y: playing_ship.arena_y - 20 }); - check.equals(targetting.target, Target.newFromShip(playing_ship), "surroundings 2"); - - targetting.setAction(playing_ship, action, ActionTargettingMode.SELF); - targetting.setTargetFromLocation({ x: 8000, y: 60 }); - check.equals(targetting.target, Target.newFromShip(playing_ship), "self 1"); - targetting.setTargetFromLocation({ x: 0, y: 0 }); - check.equals(targetting.target, Target.newFromShip(playing_ship), "self 2"); - }) - - test.case("updates the range hint display", check => { - let targetting = newTargetting(); - let ship = nn(testgame.view.battle.playing_ship); - ship.setArenaPosition(0, 0); - TestTools.setShipModel(ship, 100, 0, 8); - let move = TestTools.addEngine(ship, 100); - let fire = TestTools.addWeapon(ship, 50, 2, 300, 100); - let last_call: any = null; - check.patch(targetting.range_hint, "clear", () => { - last_call = null; - }); - check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => { - last_call = [ship, action, radius]; - }); - - // move action - targetting.setAction(ship, move); - targetting.setTargetFromLocation({ x: 200, y: 0 }); - check.equals(last_call, [ship, move, 800]); - - // fire action - targetting.setAction(ship, fire); - targetting.setTargetFromLocation({ x: 200, y: 0 }); - check.equals(last_call, [ship, fire, undefined]); - - // move+fire - targetting.setAction(ship, fire); - targetting.setTargetFromLocation({ x: 400, y: 0 }); - check.equals(last_call, [ship, move, 600]); - }); + check.patch(targetting, "simulate", () => { + let result = new MoveFireResult(); + result.success = true; + result.complete = true; + result.need_move = true; + result.move_location = Target.newFromLocation(80, 20); + result.can_move = true; + result.can_end_move = true; + result.need_fire = true; + result.can_fire = true; + result.parts = [ + { action: engine, target: Target.newFromLocation(80, 20), ap: 1, possible: true }, + { action: weapon, target: Target.newFromLocation(156, 65), ap: 5, possible: true } + ] + targetting.simulation = result; }); -} + targetting.update(); + + check.equals(targetting.container.visible, true); + check.equals(targetting.drawn_info.visible, true); + check.equals(targetting.fire_arrow.visible, true); + check.equals(targetting.fire_arrow.x, 156); + check.equals(targetting.fire_arrow.y, 65); + check.nears(targetting.fire_arrow.rotation, 0.534594, 5); + check.equals(targetting.impact_area.visible, true); + check.equals(targetting.impact_area.x, 156); + check.equals(targetting.impact_area.y, 65); + check.equals(targetting.move_ghost.visible, true); + check.equals(targetting.move_ghost.x, 80); + check.equals(targetting.move_ghost.y, 20); + check.nears(targetting.move_ghost.rotation, 0.534594, 5); + }) + + test.case("snaps on ships according to targetting mode", check => { + let targetting = newTargetting(); + let playing_ship = nn(testgame.view.battle.playing_ship); + let action = TestTools.addWeapon(playing_ship); + + let ship1 = testgame.view.battle.play_order[1]; + let ship2 = testgame.view.battle.play_order[2]; + ship1.setArenaPosition(8000, 50); + ship2.setArenaPosition(8000, 230); + + targetting.setAction(playing_ship, action, ActionTargettingMode.SPACE); + targetting.setTargetFromLocation({ x: 8000, y: 60 }); + check.equals(targetting.target, Target.newFromLocation(8000, 60), "space"); + + targetting.setAction(playing_ship, action, ActionTargettingMode.SHIP); + targetting.setTargetFromLocation({ x: 8000, y: 60 }); + check.equals(targetting.target, Target.newFromShip(ship1), "ship 1"); + targetting.setTargetFromLocation({ x: 8100, y: 200 }); + check.equals(targetting.target, Target.newFromShip(ship2), "ship 2"); + + targetting.setAction(playing_ship, action, ActionTargettingMode.SURROUNDINGS); + targetting.setTargetFromLocation({ x: 8000, y: 60 }); + check.equals(targetting.target, new Target(8000, 60, playing_ship), "surroundings 1"); + targetting.setTargetFromLocation({ x: playing_ship.arena_x + 10, y: playing_ship.arena_y - 20 }); + check.equals(targetting.target, Target.newFromShip(playing_ship), "surroundings 2"); + + targetting.setAction(playing_ship, action, ActionTargettingMode.SELF); + targetting.setTargetFromLocation({ x: 8000, y: 60 }); + check.equals(targetting.target, Target.newFromShip(playing_ship), "self 1"); + targetting.setTargetFromLocation({ x: 0, y: 0 }); + check.equals(targetting.target, Target.newFromShip(playing_ship), "self 2"); + }) + + test.case("updates the range hint display", check => { + let targetting = newTargetting(); + let ship = nn(testgame.view.battle.playing_ship); + ship.setArenaPosition(0, 0); + TestTools.setShipModel(ship, 100, 0, 8); + let move = TestTools.addEngine(ship, 100); + let fire = TestTools.addWeapon(ship, 50, 2, 300, 100); + let last_call: any = null; + check.patch(targetting.range_hint, "clear", () => { + last_call = null; + }); + check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => { + last_call = [ship, action, radius]; + }); + + // move action + targetting.setAction(ship, move); + targetting.setTargetFromLocation({ x: 200, y: 0 }); + check.equals(last_call, [ship, move, 800]); + + // fire action + targetting.setAction(ship, fire); + targetting.setTargetFromLocation({ x: 200, y: 0 }); + check.equals(last_call, [ship, fire, undefined]); + + // move+fire + targetting.setAction(ship, fire); + targetting.setTargetFromLocation({ x: 400, y: 0 }); + check.equals(last_call, [ship, move, 600]); + }); +}); diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 5e8f20f..3810900 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -1,423 +1,443 @@ -module TK.SpaceTac.UI { - /** - * Targetting system on the arena - * - * This system handles choosing a target for currently selected action, and displays a visual aid. - */ - export class Targetting { - // Container group - container: UIContainer +import { imaterialize } from "../../common/Iterators" +import { Toggle, ToggleClient } from "../../common/Toggle" +import { acopy, bool, first, minBy, nn } from "../../common/Tools" +import { ActionTargettingMode, BaseAction } from "../../core/actions/BaseAction" +import { DeployDroneAction } from "../../core/actions/DeployDroneAction" +import { MoveAction } from "../../core/actions/MoveAction" +import { ToggleAction } from "../../core/actions/ToggleAction" +import { TriggerAction } from "../../core/actions/TriggerAction" +import { arenaAngle, arenaDistance, ArenaLocation, IArenaLocation } from "../../core/ArenaLocation" +import { BaseBattleDiff } from "../../core/diffs/BaseBattleDiff" +import { ShipMoveDiff } from "../../core/diffs/ShipMoveDiff" +import { MoveFirePart, MoveFireResult, MoveFireSimulator } from "../../core/MoveFireSimulator" +import { Ship } from "../../core/Ship" +import { Target } from "../../core/Target" +import { BaseView } from "../BaseView" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { UIGraphics } from "../common/UIGraphics" +import { UIImage } from "../common/UIImage" +import { ActionBar } from "./ActionBar" +import { RangeHint } from "./RangeHint" - // Current action - ship: Ship | null = null - action: BaseAction | null = null - target: Target | null = null - mode?: ActionTargettingMode +/** + * Targetting system on the arena + * + * This system handles choosing a target for currently selected action, and displays a visual aid. + */ +export class Targetting { + // Container group + container: UIContainer - // Simulated result - simulation = new MoveFireResult() - simulated_diffs: BaseBattleDiff[] = [] + // Current action + ship: Ship | null = null + action: BaseAction | null = null + target: Target | null = null + mode?: ActionTargettingMode - // Move and fire lines - drawn_info: UIGraphics - move_ghost: UIImage - fire_arrow: UIImage + // Simulated result + simulation = new MoveFireResult() + simulated_diffs: BaseBattleDiff[] = [] - // Impact area - impact_area: UIGraphics - impact_indicators: UIContainer + // Move and fire lines + drawn_info: UIGraphics + move_ghost: UIImage + fire_arrow: UIImage - // Diffs display - diffs_move: UIGraphics + // Impact area + impact_area: UIGraphics + impact_indicators: UIContainer - // Collaborators to update - actionbar: ActionBar - range_hint: RangeHint - tactical_mode: ToggleClient + // Diffs display + diffs_move: UIGraphics - // Access to the parent view - view: BaseView + // Collaborators to update + actionbar: ActionBar + range_hint: RangeHint + tactical_mode: ToggleClient - constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle, range_hint: RangeHint) { - this.view = view; - this.actionbar = actionbar; - this.tactical_mode = tactical_mode.manipulate("targetting"); - this.range_hint = range_hint; + // Access to the parent view + view: BaseView - let builder = new UIBuilder(view); - this.container = builder.container("targetting"); - builder = builder.in(this.container); + constructor(view: BaseView, actionbar: ActionBar, tactical_mode: Toggle, range_hint: RangeHint) { + this.view = view; + this.actionbar = actionbar; + this.tactical_mode = tactical_mode.manipulate("targetting"); + this.range_hint = range_hint; - // Visual effects - this.impact_area = builder.graphics("impact-area", 0, 0, false); - this.diffs_move = builder.graphics("effects-move"); - this.drawn_info = builder.graphics("lines", 0, 0, false); - this.move_ghost = builder.image("common-transparent", 0, 0, true); - this.move_ghost.setAlpha(0.8); - this.move_ghost.setVisible(false); - this.fire_arrow = builder.image("battle-hud-simulator-ok"); - this.fire_arrow.setOrigin(1, 0.5); - this.fire_arrow.setVisible(false); - this.impact_indicators = builder.container("impact-indicators", 0, 0, false); - } + let builder = new UIBuilder(view); + this.container = builder.container("targetting"); + builder = builder.in(this.container); - /** - * Move to a given view layer - */ - moveToLayer(layer: UIContainer): void { - layer.add(this.container); - } + // Visual effects + this.impact_area = builder.graphics("impact-area", 0, 0, false); + this.diffs_move = builder.graphics("effects-move"); + this.drawn_info = builder.graphics("lines", 0, 0, false); + this.move_ghost = builder.image("common-transparent", 0, 0, true); + this.move_ghost.setAlpha(0.8); + this.move_ghost.setVisible(false); + this.fire_arrow = builder.image("battle-hud-simulator-ok"); + this.fire_arrow.setOrigin(1, 0.5); + this.fire_arrow.setVisible(false); + this.impact_indicators = builder.container("impact-indicators", 0, 0, false); + } - /** - * Indicator that the targetting is currently active - */ - get active(): boolean { - return bool(this.ship && this.action); - } + /** + * Move to a given view layer + */ + moveToLayer(layer: UIContainer): void { + layer.add(this.container); + } - /** - * Draw a vector, with line and gradation - */ - drawVector(color: number, x1: number, y1: number, x2: number, y2: number, gradation = 0) { - let graphics = this.drawn_info; - graphics.addLine({ - start: { x: x1, y: y1 }, - end: { x: x2, y: y2 }, - width: 6, - color: color - }); - graphics.addLine({ - start: { x: x1, y: y1 }, - end: { x: x2, y: y2 }, - width: 2, - color: 0, - alpha: 0.6 - }); - graphics.setVisible(true); + /** + * Indicator that the targetting is currently active + */ + get active(): boolean { + return bool(this.ship && this.action); + } - if (gradation) { - let dx = x2 - x1; - let dy = y2 - y1; - let dist = Math.sqrt(dx * dx + dy * dy); - let angle = Math.atan2(dy, dx); - dx = Math.cos(angle); - dy = Math.sin(angle); - graphics.lineStyle(3, color); - for (let d = gradation; d <= dist; d += gradation) { - graphics.addLine({ - start: { x: x1 + dx * d + dy * 10, y: y1 + dy * d - dx * 10 }, - end: { x: x1 + dx * d - dy * 10, y: y1 + dy * d + dx * 10 }, - width: 3, - color: color, - }); - } - } - } + /** + * Draw a vector, with line and gradation + */ + drawVector(color: number, x1: number, y1: number, x2: number, y2: number, gradation = 0) { + let graphics = this.drawn_info; + graphics.addLine({ + start: { x: x1, y: y1 }, + end: { x: x2, y: y2 }, + width: 6, + color: color + }); + graphics.addLine({ + start: { x: x1, y: y1 }, + end: { x: x2, y: y2 }, + width: 2, + color: 0, + alpha: 0.6 + }); + graphics.setVisible(true); - /** - * Draw a part of the simulation - */ - drawPart(part: MoveFirePart, enabled = true, previous: MoveFirePart | null = null): void { - if (!this.ship) { - return; - } - - let move = part.action instanceof MoveAction; - let color = (enabled && part.possible) ? (move ? 0xe09c47 : 0xdc6441) : 0x8e8e8e; - let src = previous ? previous.target : this.ship.location; - let gradation = (part.action instanceof MoveAction) ? part.action.distance_per_power : 0; - this.drawVector(color, src.x, src.y, part.target.x, part.target.y, gradation); - } - - /** - * Update impact indicators (highlighting impacted ships, with success factor) - */ - updateImpactIndicators(impacts: UIContainer, ship: Ship, action: BaseAction, target: Target, source: IArenaLocation = ship.location): void { - let ships = action.getImpactedShips(ship, target, source); - if (ships.length) { - // TODO differential - let builder = new UIBuilder(this.view).in(impacts); - builder.clear(); - ships.forEach(iship => { - builder.image("battle-hud-ship-impacted", iship.arena_x, iship.arena_y, true); - }); - impacts.visible = true; - } else { - impacts.visible = false; - } - } - - /** - * Update impact graphics (area display) - */ - updateImpactArea(area: UIGraphics, action: BaseAction): void { - area.clear(); - - let color = 0; - let radius = 0; - let angle = 0; - if (action instanceof TriggerAction) { - color = 0x90481e; - if (action.angle) { - angle = (action.angle * 0.5) * Math.PI / 180; - radius = action.range; - } else { - radius = action.blast; - } - } else if (action instanceof DeployDroneAction) { - color = 0xe9f2f9; - radius = action.drone_radius; - } else if (action instanceof ToggleAction) { - color = 0xd3e448; - radius = action.radius; - } - - if (radius) { - if (angle) { - area.addCircleArc({ - radius: radius, - angle: { start: -angle, span: angle * 2 }, - fill: { color: color, alpha: 0.2 }, - border: { color: color, alpha: 0.6, width: 2 }, - }); - area.addCircleArc({ - radius: radius * 0.95, - angle: { start: -angle * 0.95, span: angle * 1.9 }, - fill: { color: color, alpha: 0.1 }, - border: { color: color, alpha: 0.3, width: 1 }, - }); - } else { - area.addCircle({ - radius: radius, - fill: { color: color, alpha: 0.2 }, - border: { color: color, alpha: 0.6, width: 2 }, - }); - area.addCircle({ - radius: radius * 0.95, - fill: { color: color, alpha: 0.1 }, - border: { color: color, alpha: 0.3, width: 1 }, - }); - } - } - } - - /** - * Update information about simulated diffs - */ - updateDiffsDisplay(): void { - this.diffs_move.clear(); - - this.simulated_diffs.forEach(diff => { - if (diff instanceof ShipMoveDiff) { - this.diffs_move.addLine({ - start: diff.start, - end: diff.end, - width: 4, - color: 0xFFFFFF, - alpha: 0.5 - }); - } - }); - } - - /** - * Update visual effects to show the simulation of current action/target - */ - update(): void { - this.simulate(); - if (this.ship && this.action && this.target) { - let simulation = this.simulation; - - this.drawn_info.clear(); - this.fire_arrow.visible = false; - this.move_ghost.visible = false; - - let from = (simulation.need_fire && this.mode != ActionTargettingMode.SURROUNDINGS) ? simulation.move_location : this.ship.location; - let angle = Math.atan2(this.target.y - from.y, this.target.x - from.x); - - if (simulation.success) { - let previous: MoveFirePart | null = null; - simulation.parts.forEach(part => { - this.drawPart(part, simulation.complete, previous); - previous = part; - }); - - if (simulation.need_move) { - this.move_ghost.visible = true; - this.move_ghost.setPosition(simulation.move_location.x, simulation.move_location.y); - this.move_ghost.rotation = angle; - } else { - this.move_ghost.visible = false; - } - - if (simulation.need_fire) { - if (this.action instanceof TriggerAction && this.action.angle) { - this.impact_area.setPosition(simulation.move_location.x, simulation.move_location.y); - this.impact_area.setRotation(arenaAngle(simulation.move_location, simulation.fire_location)); - } else { - this.impact_area.setPosition(this.target.x, this.target.y); - } - this.impact_area.alpha = simulation.can_fire ? 1 : 0.5; - this.impact_area.visible = true; - - this.updateImpactIndicators(this.impact_indicators, this.ship, this.action, this.target, this.simulation.move_location); - - this.fire_arrow.setPosition(this.target.x, this.target.y); - this.fire_arrow.setRotation(angle); - this.view.changeImage(this.fire_arrow, simulation.complete ? "battle-hud-simulator-ok" : "battle-hud-simulator-power"); - this.fire_arrow.visible = true; - } else { - this.impact_area.visible = false; - this.impact_indicators.visible = false; - this.fire_arrow.visible = false; - } - } else { - this.drawVector(0x888888, this.ship.arena_x, this.ship.arena_y, this.target.x, this.target.y); - this.fire_arrow.setPosition(this.target.x, this.target.y); - this.fire_arrow.setRotation(angle); - this.view.changeImage(this.fire_arrow, "battle-hud-simulator-failed"); - this.fire_arrow.visible = true; - this.impact_area.visible = false; - } - - this.updateDiffsDisplay(); - - this.container.visible = true; - } else { - this.container.visible = false; - } - - // Toggle tactical mode - this.tactical_mode(bool(this.action)); - - // Toggle range hint - if (this.ship && this.action) { - if (this.simulation.need_move) { - let move_action: MoveAction | null = null; - if (this.simulation.success) { - let last_move = first(acopy(this.simulation.parts).reverse(), part => part.action instanceof MoveAction); - if (last_move) { - move_action = last_move.action; - } - } else { - let engine = new MoveFireSimulator(this.ship).findEngine(); - if (engine) { - move_action = engine; - } - } - if (move_action) { - let power = this.ship.getValue("power"); - if (this.action !== move_action) { - power = Math.max(power - this.action.getPowerUsage(this.ship, this.target), 0); - } - let radius = move_action.getRangeRadiusForPower(this.ship, power); - this.range_hint.update(this.ship, move_action, radius); - } else { - this.range_hint.clear(); - } - } else { - this.range_hint.update(this.ship, this.action); - } - } else { - this.range_hint.clear(); - } - } - - /** - * Simulate current action - */ - simulate(): void { - if (this.ship && this.action && this.target) { - let battle = nn(this.ship.getBattle()); - let simulator = new MoveFireSimulator(this.ship); - this.simulation = simulator.simulateAction(this.action, this.target, 1); - this.simulated_diffs = simulator.getExpectedDiffs(battle, this.simulation); - } else { - this.simulation = new MoveFireResult(); - this.simulated_diffs = []; - } - } - - /** - * Set the current targetting action, or null to stop targetting - */ - setAction(ship: Ship | null, action: BaseAction | null, mode?: ActionTargettingMode): void { - if (action && ship && ship.actions.getById(action.id)) { - this.ship = ship; - this.action = action; - this.mode = (typeof mode == "undefined") ? action.getTargettingMode(this.ship) : mode; - - this.view.changeImage(this.move_ghost, `ship-${this.ship.model.code}-sprite`); - - this.updateImpactArea(this.impact_area, this.action); - - this.setTarget(action.getDefaultTarget(this.ship)); - } else { - this.ship = null; - this.action = null; - - this.setTarget(null); - } - } - - /** - * Set the target according to a hovered arena location - * - * This will apply the current targetting mode, to assist the player - */ - setTargetFromLocation(location: ArenaLocation | null): void { - if (location && this.ship) { - let battle = this.ship.getBattle(); - if (this.mode == ActionTargettingMode.SHIP && battle) { - let targets = imaterialize(battle.iships(true)); - let nearest = minBy(targets, ship => arenaDistance(ship.location, location)); - this.setTarget(Target.newFromShip(nearest ? nearest : this.ship)); - } else if (this.mode == ActionTargettingMode.SPACE) { - this.setTarget(Target.newFromLocation(location.x, location.y)); - } else if (this.mode == ActionTargettingMode.SURROUNDINGS) { - if (arenaDistance(this.ship.location, location) < 50) { - this.setTarget(Target.newFromShip(this.ship)); - } else { - this.setTarget(new Target(location.x, location.y, this.ship)); - } - } else { - this.setTarget(Target.newFromShip(this.ship)); - } - } else { - this.setTarget(null); - } - } - - /** - * Set the target for current action - */ - setTarget(target: Target | null): void { - this.target = target; - this.update(); - if (this.action) { - this.actionbar.updateFromSimulation(this.action, this.simulation); - } - } - - /** - * Validate the current target. - * - * This will make the needed approach and apply the action. - */ - validate(applier: (action: BaseAction, target?: Target) => boolean): void { - if (this.active) { - this.simulate(); - - if (this.ship && this.simulation.complete) { - let ship = this.ship; - this.simulation.parts.forEach(part => { - if (part.possible) { - applier(part.action, part.target); - } - }); - this.actionbar.actionEnded(); - } - } - } + if (gradation) { + let dx = x2 - x1; + let dy = y2 - y1; + let dist = Math.sqrt(dx * dx + dy * dy); + let angle = Math.atan2(dy, dx); + dx = Math.cos(angle); + dy = Math.sin(angle); + graphics.lineStyle(3, color); + for (let d = gradation; d <= dist; d += gradation) { + graphics.addLine({ + start: { x: x1 + dx * d + dy * 10, y: y1 + dy * d - dx * 10 }, + end: { x: x1 + dx * d - dy * 10, y: y1 + dy * d + dx * 10 }, + width: 3, + color: color, + }); + } } + } + + /** + * Draw a part of the simulation + */ + drawPart(part: MoveFirePart, enabled = true, previous: MoveFirePart | null = null): void { + if (!this.ship) { + return; + } + + let move = part.action instanceof MoveAction; + let color = (enabled && part.possible) ? (move ? 0xe09c47 : 0xdc6441) : 0x8e8e8e; + let src = previous ? previous.target : this.ship.location; + let gradation = (part.action instanceof MoveAction) ? part.action.distance_per_power : 0; + this.drawVector(color, src.x, src.y, part.target.x, part.target.y, gradation); + } + + /** + * Update impact indicators (highlighting impacted ships, with success factor) + */ + updateImpactIndicators(impacts: UIContainer, ship: Ship, action: BaseAction, target: Target, source: IArenaLocation = ship.location): void { + let ships = action.getImpactedShips(ship, target, source); + if (ships.length) { + // TODO differential + let builder = new UIBuilder(this.view).in(impacts); + builder.clear(); + ships.forEach(iship => { + builder.image("battle-hud-ship-impacted", iship.arena_x, iship.arena_y, true); + }); + impacts.visible = true; + } else { + impacts.visible = false; + } + } + + /** + * Update impact graphics (area display) + */ + updateImpactArea(area: UIGraphics, action: BaseAction): void { + area.clear(); + + let color = 0; + let radius = 0; + let angle = 0; + if (action instanceof TriggerAction) { + color = 0x90481e; + if (action.angle) { + angle = (action.angle * 0.5) * Math.PI / 180; + radius = action.range; + } else { + radius = action.blast; + } + } else if (action instanceof DeployDroneAction) { + color = 0xe9f2f9; + radius = action.drone_radius; + } else if (action instanceof ToggleAction) { + color = 0xd3e448; + radius = action.radius; + } + + if (radius) { + if (angle) { + area.addCircleArc({ + radius: radius, + angle: { start: -angle, span: angle * 2 }, + fill: { color: color, alpha: 0.2 }, + border: { color: color, alpha: 0.6, width: 2 }, + }); + area.addCircleArc({ + radius: radius * 0.95, + angle: { start: -angle * 0.95, span: angle * 1.9 }, + fill: { color: color, alpha: 0.1 }, + border: { color: color, alpha: 0.3, width: 1 }, + }); + } else { + area.addCircle({ + radius: radius, + fill: { color: color, alpha: 0.2 }, + border: { color: color, alpha: 0.6, width: 2 }, + }); + area.addCircle({ + radius: radius * 0.95, + fill: { color: color, alpha: 0.1 }, + border: { color: color, alpha: 0.3, width: 1 }, + }); + } + } + } + + /** + * Update information about simulated diffs + */ + updateDiffsDisplay(): void { + this.diffs_move.clear(); + + this.simulated_diffs.forEach(diff => { + if (diff instanceof ShipMoveDiff) { + this.diffs_move.addLine({ + start: diff.start, + end: diff.end, + width: 4, + color: 0xFFFFFF, + alpha: 0.5 + }); + } + }); + } + + /** + * Update visual effects to show the simulation of current action/target + */ + update(): void { + this.simulate(); + if (this.ship && this.action && this.target) { + let simulation = this.simulation; + + this.drawn_info.clear(); + this.fire_arrow.visible = false; + this.move_ghost.visible = false; + + let from = (simulation.need_fire && this.mode != ActionTargettingMode.SURROUNDINGS) ? simulation.move_location : this.ship.location; + let angle = Math.atan2(this.target.y - from.y, this.target.x - from.x); + + if (simulation.success) { + let previous: MoveFirePart | null = null; + simulation.parts.forEach(part => { + this.drawPart(part, simulation.complete, previous); + previous = part; + }); + + if (simulation.need_move) { + this.move_ghost.visible = true; + this.move_ghost.setPosition(simulation.move_location.x, simulation.move_location.y); + this.move_ghost.rotation = angle; + } else { + this.move_ghost.visible = false; + } + + if (simulation.need_fire) { + if (this.action instanceof TriggerAction && this.action.angle) { + this.impact_area.setPosition(simulation.move_location.x, simulation.move_location.y); + this.impact_area.setRotation(arenaAngle(simulation.move_location, simulation.fire_location)); + } else { + this.impact_area.setPosition(this.target.x, this.target.y); + } + this.impact_area.alpha = simulation.can_fire ? 1 : 0.5; + this.impact_area.visible = true; + + this.updateImpactIndicators(this.impact_indicators, this.ship, this.action, this.target, this.simulation.move_location); + + this.fire_arrow.setPosition(this.target.x, this.target.y); + this.fire_arrow.setRotation(angle); + this.view.changeImage(this.fire_arrow, simulation.complete ? "battle-hud-simulator-ok" : "battle-hud-simulator-power"); + this.fire_arrow.visible = true; + } else { + this.impact_area.visible = false; + this.impact_indicators.visible = false; + this.fire_arrow.visible = false; + } + } else { + this.drawVector(0x888888, this.ship.arena_x, this.ship.arena_y, this.target.x, this.target.y); + this.fire_arrow.setPosition(this.target.x, this.target.y); + this.fire_arrow.setRotation(angle); + this.view.changeImage(this.fire_arrow, "battle-hud-simulator-failed"); + this.fire_arrow.visible = true; + this.impact_area.visible = false; + } + + this.updateDiffsDisplay(); + + this.container.visible = true; + } else { + this.container.visible = false; + } + + // Toggle tactical mode + this.tactical_mode(bool(this.action)); + + // Toggle range hint + if (this.ship && this.action) { + if (this.simulation.need_move) { + let move_action: MoveAction | null = null; + if (this.simulation.success) { + let last_move = first(acopy(this.simulation.parts).reverse(), part => part.action instanceof MoveAction); + if (last_move) { + move_action = last_move.action; + } + } else { + let engine = new MoveFireSimulator(this.ship).findEngine(); + if (engine) { + move_action = engine; + } + } + if (move_action) { + let power = this.ship.getValue("power"); + if (this.action !== move_action) { + power = Math.max(power - this.action.getPowerUsage(this.ship, this.target), 0); + } + let radius = move_action.getRangeRadiusForPower(this.ship, power); + this.range_hint.update(this.ship, move_action, radius); + } else { + this.range_hint.clear(); + } + } else { + this.range_hint.update(this.ship, this.action); + } + } else { + this.range_hint.clear(); + } + } + + /** + * Simulate current action + */ + simulate(): void { + if (this.ship && this.action && this.target) { + let battle = nn(this.ship.getBattle()); + let simulator = new MoveFireSimulator(this.ship); + this.simulation = simulator.simulateAction(this.action, this.target, 1); + this.simulated_diffs = simulator.getExpectedDiffs(battle, this.simulation); + } else { + this.simulation = new MoveFireResult(); + this.simulated_diffs = []; + } + } + + /** + * Set the current targetting action, or null to stop targetting + */ + setAction(ship: Ship | null, action: BaseAction | null, mode?: ActionTargettingMode): void { + if (action && ship && ship.actions.getById(action.id)) { + this.ship = ship; + this.action = action; + this.mode = (typeof mode == "undefined") ? action.getTargettingMode(this.ship) : mode; + + this.view.changeImage(this.move_ghost, `ship-${this.ship.model.code}-sprite`); + + this.updateImpactArea(this.impact_area, this.action); + + this.setTarget(action.getDefaultTarget(this.ship)); + } else { + this.ship = null; + this.action = null; + + this.setTarget(null); + } + } + + /** + * Set the target according to a hovered arena location + * + * This will apply the current targetting mode, to assist the player + */ + setTargetFromLocation(location: ArenaLocation | null): void { + if (location && this.ship) { + let battle = this.ship.getBattle(); + if (this.mode == ActionTargettingMode.SHIP && battle) { + let targets = imaterialize(battle.iships(true)); + let nearest = minBy(targets, ship => arenaDistance(ship.location, location)); + this.setTarget(Target.newFromShip(nearest ? nearest : this.ship)); + } else if (this.mode == ActionTargettingMode.SPACE) { + this.setTarget(Target.newFromLocation(location.x, location.y)); + } else if (this.mode == ActionTargettingMode.SURROUNDINGS) { + if (arenaDistance(this.ship.location, location) < 50) { + this.setTarget(Target.newFromShip(this.ship)); + } else { + this.setTarget(new Target(location.x, location.y, this.ship)); + } + } else { + this.setTarget(Target.newFromShip(this.ship)); + } + } else { + this.setTarget(null); + } + } + + /** + * Set the target for current action + */ + setTarget(target: Target | null): void { + this.target = target; + this.update(); + if (this.action) { + this.actionbar.updateFromSimulation(this.action, this.simulation); + } + } + + /** + * Validate the current target. + * + * This will make the needed approach and apply the action. + */ + validate(applier: (action: BaseAction, target?: Target) => boolean): void { + if (this.active) { + this.simulate(); + + if (this.ship && this.simulation.complete) { + let ship = this.ship; + this.simulation.parts.forEach(part => { + if (part.possible) { + applier(part.action, part.target); + } + }); + this.actionbar.actionEnded(); + } + } + } } diff --git a/src/ui/battle/WeaponEffect.spec.ts b/src/ui/battle/WeaponEffect.spec.ts index b9a4525..66fe926 100644 --- a/src/ui/battle/WeaponEffect.spec.ts +++ b/src/ui/battle/WeaponEffect.spec.ts @@ -1,124 +1,133 @@ -module TK.SpaceTac.UI.Specs { - testing("WeaponEffect", test => { - let testgame = setupBattleview(test); +import { testing } from "../../common/Testing"; +import { Timer } from "../../common/Timer"; +import { nn } from "../../common/Tools"; +import { TriggerAction } from "../../core/actions/TriggerAction"; +import { DamageEffect } from "../../core/effects/DamageEffect"; +import { Ship } from "../../core/Ship"; +import { Target } from "../../core/Target"; +import { UIImage } from "../common/UIImage"; +import { checkTween, setupBattleview } from "../TestGame"; +import { WeaponEffect } from "./WeaponEffect"; - function checkEmitters(step: string, expected: number) { - test.check.same(testgame.view.arena.layer_weapon_effects.length, expected, `${step} - layer children`); - //test.check.same(keys(testgame.view.particles.emitters).length, expected, `${step} - registered emitters`); - } +testing("WeaponEffect", test => { + let testgame = setupBattleview(test); - test.case("displays shield hit effect", check => { - let battleview = testgame.view; - battleview.timer = new Timer(); + function checkEmitters(step: string, expected: number) { + test.check.same(testgame.view.arena.layer_weapon_effects.length, expected, `${step} - layer children`); + //test.check.same(keys(testgame.view.particles.emitters).length, expected, `${step} - registered emitters`); + } - let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon")); - effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 1, true); + test.case("displays shield hit effect", check => { + let battleview = testgame.view; + battleview.timer = new Timer(); - let layer = battleview.arena.layer_weapon_effects; - check.equals(layer.length, 2); + let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon")); + effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 1, true); - check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); + let layer = battleview.arena.layer_weapon_effects; + check.equals(layer.length, 2); - let child = layer.list[1]; - if (check.instance(child, UIImage, "second child is an image")) { - check.nears(child.rotation, -2.677945044588987, 10); - check.equals(child.x, 20, "x"); - check.equals(child.y, 15, "y"); - } - }); + check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); - test.case("displays gatling gun effect", check => { - let battleview = testgame.view; - battleview.timer = new Timer(); + let child = layer.list[1]; + if (check.instance(child, UIImage, "second child is an image")) { + check.nears(child.rotation, -2.677945044588987, 10); + check.equals(child.x, 20, "x"); + check.equals(child.y, 15, "y"); + } + }); - let ship = nn(battleview.battle.playing_ship); - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new TriggerAction("weapon")); - effect.bulletsExecutor(1); + test.case("displays gatling gun effect", check => { + let battleview = testgame.view; + battleview.timer = new Timer(); - let layer = battleview.arena.layer_weapon_effects; - check.equals(layer.length, 1); - check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); - }); + let ship = nn(battleview.battle.playing_ship); + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new TriggerAction("weapon")); + effect.bulletsExecutor(1); - test.acase("displays shield and hull effect on impacted ships", async check => { - let battleview = testgame.view; - battleview.timer = new Timer(); + let layer = battleview.arena.layer_weapon_effects; + check.equals(layer.length, 1); + check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter"); + }); - let ship = nn(battleview.battle.playing_ship); - ship.setArenaPosition(50, 30); + test.acase("displays shield and hull effect on impacted ships", async check => { + let battleview = testgame.view; + battleview.timer = new Timer(); - let weapon = new TriggerAction("weapon", { effects: [new DamageEffect(1)], range: 500 }); - check.patch(weapon, "getImpactedShips", () => [ship]); + let ship = nn(battleview.battle.playing_ship); + ship.setArenaPosition(50, 30); - let dest = new Ship(); - let effect = new WeaponEffect(battleview.arena, dest, Target.newFromShip(dest), weapon); - check.patch(effect, "getEffectForWeapon", () => { - return { - execution: () => Promise.resolve(), - delay: () => 0 - }; - }); + let weapon = new TriggerAction("weapon", { effects: [new DamageEffect(1)], range: 500 }); + check.patch(weapon, "getImpactedShips", () => [ship]); - let mock_shield_impact = check.patch(effect, "shieldImpactEffect", null); - let mock_hull_impact = check.patch(effect, "hullImpactEffect", null); - - ship.setValue("shield", 0); - await effect.start(1); - check.called(mock_shield_impact, 0); - check.called(mock_hull_impact, [ - [Target.newFromShip(dest), ship.location, 1] - ]); - - ship.setValue("shield", 10); - await effect.start(2); - check.called(mock_shield_impact, [ - [Target.newFromShip(dest), ship.location, 2, false] - ]); - check.called(mock_hull_impact, 0); - }); - - test.case("removes particle emitters when done", check => { - let battleview = testgame.view; - battleview.timer = new Timer(); - - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new TriggerAction("weapon")); - - effect.bulletsExecutor(1); - checkEmitters("gun effect started", 1); - testgame.clockForward(6000); - checkEmitters("gun effect ended", 0); - - effect.hullImpactEffect({ x: 0, y: 0 }, { x: 50, y: 50 }, 1); - checkEmitters("hull effect started", 1); - testgame.clockForward(8500); - checkEmitters("hull effect ended", 0); - }); - - test.case("adds a laser effect", check => { - let battleview = testgame.view; - battleview.timer = new Timer(); - - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon")); - effect.source = { x: 20, y: 30 }; - effect.action.angle = 90; - effect.action.range = 300; - effect.laserExecutor(5); - - let layer = battleview.arena.layer_weapon_effects; - check.equals(layer.length, 1); - let image = layer.list[0]; - if (check.instance(image, UIImage, "first child is an image")) { - check.equals(image.name, "battle-effects-laser"); - //check.equals(image.width, 300); - check.equals(image.x, 20); - check.equals(image.y, 30); - check.nears(image.rotation, 0.2606023917473408); - - checkTween(testgame, image, "rotation", [ - 0.2606023917473408, - 1.8313987185422373, - ]); - } - }); + let dest = new Ship(); + let effect = new WeaponEffect(battleview.arena, dest, Target.newFromShip(dest), weapon); + check.patch(effect, "getEffectForWeapon", () => { + return { + execution: () => Promise.resolve(), + delay: () => 0 + }; }); -} + + let mock_shield_impact = check.patch(effect, "shieldImpactEffect", null); + let mock_hull_impact = check.patch(effect, "hullImpactEffect", null); + + ship.setValue("shield", 0); + await effect.start(1); + check.called(mock_shield_impact, 0); + check.called(mock_hull_impact, [ + [Target.newFromShip(dest), ship.location, 1] + ]); + + ship.setValue("shield", 10); + await effect.start(2); + check.called(mock_shield_impact, [ + [Target.newFromShip(dest), ship.location, 2, false] + ]); + check.called(mock_hull_impact, 0); + }); + + test.case("removes particle emitters when done", check => { + let battleview = testgame.view; + battleview.timer = new Timer(); + + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new TriggerAction("weapon")); + + effect.bulletsExecutor(1); + checkEmitters("gun effect started", 1); + testgame.clockForward(6000); + checkEmitters("gun effect ended", 0); + + effect.hullImpactEffect({ x: 0, y: 0 }, { x: 50, y: 50 }, 1); + checkEmitters("hull effect started", 1); + testgame.clockForward(8500); + checkEmitters("hull effect ended", 0); + }); + + test.case("adds a laser effect", check => { + let battleview = testgame.view; + battleview.timer = new Timer(); + + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon")); + effect.source = { x: 20, y: 30 }; + effect.action.angle = 90; + effect.action.range = 300; + effect.laserExecutor(5); + + let layer = battleview.arena.layer_weapon_effects; + check.equals(layer.length, 1); + let image = layer.list[0]; + if (check.instance(image, UIImage, "first child is an image")) { + check.equals(image.name, "battle-effects-laser"); + //check.equals(image.width, 300); + check.equals(image.x, 20); + check.equals(image.y, 30); + check.nears(image.rotation, 0.2606023917473408); + + checkTween(testgame, image, "rotation", [ + 0.2606023917473408, + 1.8313987185422373, + ]); + } + }); +}); diff --git a/src/ui/battle/WeaponEffect.ts b/src/ui/battle/WeaponEffect.ts index bb7b1d0..43936cc 100644 --- a/src/ui/battle/WeaponEffect.ts +++ b/src/ui/battle/WeaponEffect.ts @@ -1,264 +1,275 @@ -module TK.SpaceTac.UI { - type WeaponEffectInfo = { - execution: (speed: number) => Promise - delay: (ship: Ship) => number - } +import { Timer } from "../../common/Timer" +import { any } from "../../common/Tools" +import { TriggerAction } from "../../core/actions/TriggerAction" +import { angularDifference, arenaAngle, arenaDistance, IArenaLocation, radians } from "../../core/ArenaLocation" +import { DamageEffect } from "../../core/effects/DamageEffect" +import { Ship } from "../../core/Ship" +import { Target } from "../../core/Target" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { ParticleFacingMode } from "../common/UIParticles" +import { Arena } from "./Arena" +import { BattleView } from "./BattleView" - /** - * Visual effects renderer for weapons. - */ - export class WeaponEffect { - // Link to view - private view: BattleView - - // Timer to use - private timer: Timer - - // Display group in which to display the visual effects - private layer: UIContainer - - // Builder for images - private builder: UIBuilder - - // Firing ship - ship: Ship - source: IArenaLocation - - // Target (ship or space) - target: Target - destination: IArenaLocation - - // Weapon used - action: TriggerAction - - constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { - this.view = arena.view; - this.timer = arena.view.timer; - this.layer = arena.layer_weapon_effects; - this.builder = new UIBuilder(arena.view, this.layer); - this.ship = ship; - this.target = target; - this.action = action; - - this.source = Target.newFromShip(this.ship); - this.destination = this.target; - } - - /** - * Start the visual effect - */ - async start(speed: number): Promise { - if (!speed) { - return; - } - - // Fire effect - let fire_effect = this.getEffectForWeapon(this.action.code, this.action); - let promises = [fire_effect.execution(speed)]; - - // Damage effect - let action = this.action; - if (any(action.effects, effect => effect instanceof DamageEffect)) { - let ships = action.getImpactedShips(this.ship, this.target, this.source).map((ship): [Ship, number] => { - return [ship, fire_effect.delay(ship) / speed]; - }); - let source = action.blast ? this.target : this.source; - promises.push(this.damageEffect(source, ships, speed, this.action.code == "gatlinggun")); - } - - await Promise.all(promises); - } - - /** - * Add a damage effect on ships impacted by a weapon - */ - async damageEffect(source: IArenaLocation, ships: [Ship, number][], speed = 1, shield_flares = false): Promise { - let promises = ships.map(([ship, delay]) => { - return this.timer.sleep(delay).then(() => { - if (ship.getValue("shield") > 0) { - return this.shieldImpactEffect(source, ship.location, speed, shield_flares); - } else { - return this.hullImpactEffect(source, ship.location, speed); - } - }); - }); - - await Promise.all(promises); - } - - /** - * Get the function that will be called to start the visual effect - */ - getEffectForWeapon(weapon: string, action: TriggerAction): WeaponEffectInfo { - switch (weapon) { - case "gatlinggun": - return this.bulletsEffect(); - case "prokhorovlaser": - return this.laserEffect(); - default: - return this.genericEffect(); - } - } - - /** - * Add a shield impact effect on a ship - */ - async shieldImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1, particles = false): Promise { - let angle = Math.atan2(from.y - ship.y, from.x - ship.x); - - if (particles) { - this.builder.particles({ - key: "battle-effects-hot", - source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 10 }, - emitDuration: 500 / speed, - count: 50, - lifetime: 400 / speed, - fading: true, - direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, - scale: { min: 0.7, max: 1.2 }, - speed: { min: 20 / speed, max: 80 / speed } - }); - } - - let effect = this.builder.image("battle-effects-shield-impact", ship.x, ship.y, true); - effect.setAlpha(0); - effect.setRotation(angle); - await this.view.animations.addAnimation(effect, { alpha: 1 }, 100 / speed, undefined); - await this.timer.sleep(800 / speed); - await this.view.animations.addAnimation(effect, { alpha: 0 }, 100 / speed, undefined); - effect.destroy(); - } - - /** - * Add a hull impact effect on a ship - */ - async hullImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1): Promise { - let angle = Math.atan2(from.y - ship.y, from.x - ship.x); - - this.builder.particles({ - key: "battle-effects-hot", - source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 }, - emitDuration: 500 / speed, - count: 50, - lifetime: 400 / speed, - fading: true, - direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, - scale: { min: 1, max: 2 }, - speed: { min: 120 / speed, max: 260 / speed } - }); - - return Promise.resolve(); // TODO - } - - /** - * Generic weapon effect - */ - async genericExecutor(speed: number): Promise { - this.view.audio.playOnce("battle-weapon-missile-launch", speed); - - let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true); - missile.setRotation(arenaAngle(this.source, this.destination)); - - let blast_radius = this.action.blast; - - let projectile_duration = (arenaDistance(this.source, this.destination) * 1.5) || 1; - await this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration / speed); - missile.destroy(); - - if (blast_radius > 0) { - this.view.audio.playOnce("battle-weapon-missile-explosion", speed); - - let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true); - let scaling = blast_radius * 2 / (blast.width * 0.9); - blast.setScale(0.001); - - await Promise.all([ - this.view.animations.addAnimation(blast, { alpha: 0 }, 1450 / speed, "Quad.easeIn"), - this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500 / speed, "Quint.easeOut"), - ]); - blast.destroy(); - } - } - - private genericEffect(): WeaponEffectInfo { - return { - execution: speed => this.genericExecutor(speed), - delay: ship => { - let result = (arenaDistance(this.source, this.destination) * 1.5) || 1; - if (this.action.blast) { - result += 300 * Phaser.Math.Easing.Quintic.Out(arenaDistance(this.destination, ship.location) / this.action.blast); - } - return result; - } - } - } - - /** - * Laser effect, scanning from one angle to the other - */ - laserExecutor(speed: number): Promise { - let duration = 1000 / speed; - let angle = arenaAngle(this.source, this.target); - let dangle = radians(this.action.angle) * 0.5; - - this.view.audio.playOnce("battle-weapon-laser", speed); - - let laser = this.builder.image("battle-effects-laser", this.source.x, this.source.y); - laser.setOrigin(0, 0.5); - laser.setRotation(angle - dangle); - laser.setScale(this.action.range / laser.width); - let dest_angle = laser.rotation + angularDifference(laser.rotation, angle + dangle); - return this.view.animations.addAnimation(laser, { rotation: dest_angle }, duration).then(() => laser.destroy()); - } - - private laserEffect(): WeaponEffectInfo { - return { - execution: speed => this.laserExecutor(speed), - delay: ship => { - let angle = arenaAngle(this.source, this.target); - let span = radians(this.action.angle); - return 900 * Math.abs(angularDifference(angle - span * 0.5, arenaAngle(this.source, ship.location))) / span; - } - } - } - - /** - * Submachine gun effect (quick chain of small bullets) - */ - bulletsExecutor(speed: number): Promise { - this.view.audio.playOnce("battle-weapon-bullets", speed); - - let target_ship = this.target.getShip(this.view.battle); - let has_shield = target_ship && (target_ship.getValue("shield") > 0); - - let angle = arenaAngle(this.source, this.target); - let distance = arenaDistance(this.source, this.target); - let guard = 35 + (has_shield ? 80 : 40); - if (guard + 1 > distance) { - guard = distance - 1; - } - let duration = 500 / speed; - let lifetime = (distance - guard) / (2 * speed); - this.builder.particles({ - key: "battle-effects-bullets", - source: { x: this.source.x + Math.cos(angle) * 35, y: this.source.y + Math.sin(angle) * 35, radius: 3 }, - emitDuration: duration, - count: 50, - lifetime: lifetime, - direction: { minangle: angle, maxangle: angle }, - scale: { min: 1, max: 1 }, - speed: { min: 2000 / speed, max: 2000 / speed }, - facing: ParticleFacingMode.ALWAYS - }); - - return this.timer.sleep(lifetime); - } - - private bulletsEffect(): WeaponEffectInfo { - return { - execution: speed => this.bulletsExecutor(speed), - delay: ship => 2000 / arenaDistance(this.source, ship.location) - } - } - } +type WeaponEffectInfo = { + execution: (speed: number) => Promise + delay: (ship: Ship) => number +} + +/** + * Visual effects renderer for weapons. + */ +export class WeaponEffect { + // Link to view + private view: BattleView + + // Timer to use + private timer: Timer + + // Display group in which to display the visual effects + private layer: UIContainer + + // Builder for images + private builder: UIBuilder + + // Firing ship + ship: Ship + source: IArenaLocation + + // Target (ship or space) + target: Target + destination: IArenaLocation + + // Weapon used + action: TriggerAction + + constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { + this.view = arena.view; + this.timer = arena.view.timer; + this.layer = arena.layer_weapon_effects; + this.builder = new UIBuilder(arena.view, this.layer); + this.ship = ship; + this.target = target; + this.action = action; + + this.source = Target.newFromShip(this.ship); + this.destination = this.target; + } + + /** + * Start the visual effect + */ + async start(speed: number): Promise { + if (!speed) { + return; + } + + // Fire effect + let fire_effect = this.getEffectForWeapon(this.action.code, this.action); + let promises = [fire_effect.execution(speed)]; + + // Damage effect + let action = this.action; + if (any(action.effects, effect => effect instanceof DamageEffect)) { + let ships = action.getImpactedShips(this.ship, this.target, this.source).map((ship): [Ship, number] => { + return [ship, fire_effect.delay(ship) / speed]; + }); + let source = action.blast ? this.target : this.source; + promises.push(this.damageEffect(source, ships, speed, this.action.code == "gatlinggun")); + } + + await Promise.all(promises); + } + + /** + * Add a damage effect on ships impacted by a weapon + */ + async damageEffect(source: IArenaLocation, ships: [Ship, number][], speed = 1, shield_flares = false): Promise { + let promises = ships.map(([ship, delay]) => { + return this.timer.sleep(delay).then(() => { + if (ship.getValue("shield") > 0) { + return this.shieldImpactEffect(source, ship.location, speed, shield_flares); + } else { + return this.hullImpactEffect(source, ship.location, speed); + } + }); + }); + + await Promise.all(promises); + } + + /** + * Get the function that will be called to start the visual effect + */ + getEffectForWeapon(weapon: string, action: TriggerAction): WeaponEffectInfo { + switch (weapon) { + case "gatlinggun": + return this.bulletsEffect(); + case "prokhorovlaser": + return this.laserEffect(); + default: + return this.genericEffect(); + } + } + + /** + * Add a shield impact effect on a ship + */ + async shieldImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1, particles = false): Promise { + let angle = Math.atan2(from.y - ship.y, from.x - ship.x); + + if (particles) { + this.builder.particles({ + key: "battle-effects-hot", + source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 10 }, + emitDuration: 500 / speed, + count: 50, + lifetime: 400 / speed, + fading: true, + direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, + scale: { min: 0.7, max: 1.2 }, + speed: { min: 20 / speed, max: 80 / speed } + }); + } + + let effect = this.builder.image("battle-effects-shield-impact", ship.x, ship.y, true); + effect.setAlpha(0); + effect.setRotation(angle); + await this.view.animations.addAnimation(effect, { alpha: 1 }, 100 / speed, undefined); + await this.timer.sleep(800 / speed); + await this.view.animations.addAnimation(effect, { alpha: 0 }, 100 / speed, undefined); + effect.destroy(); + } + + /** + * Add a hull impact effect on a ship + */ + async hullImpactEffect(from: IArenaLocation, ship: IArenaLocation, speed = 1): Promise { + let angle = Math.atan2(from.y - ship.y, from.x - ship.x); + + this.builder.particles({ + key: "battle-effects-hot", + source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 }, + emitDuration: 500 / speed, + count: 50, + lifetime: 400 / speed, + fading: true, + direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 }, + scale: { min: 1, max: 2 }, + speed: { min: 120 / speed, max: 260 / speed } + }); + + return Promise.resolve(); // TODO + } + + /** + * Generic weapon effect + */ + async genericExecutor(speed: number): Promise { + this.view.audio.playOnce("battle-weapon-missile-launch", speed); + + let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true); + missile.setRotation(arenaAngle(this.source, this.destination)); + + let blast_radius = this.action.blast; + + let projectile_duration = (arenaDistance(this.source, this.destination) * 1.5) || 1; + await this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration / speed); + missile.destroy(); + + if (blast_radius > 0) { + this.view.audio.playOnce("battle-weapon-missile-explosion", speed); + + let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true); + let scaling = blast_radius * 2 / (blast.width * 0.9); + blast.setScale(0.001); + + await Promise.all([ + this.view.animations.addAnimation(blast, { alpha: 0 }, 1450 / speed, "Quad.easeIn"), + this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500 / speed, "Quint.easeOut"), + ]); + blast.destroy(); + } + } + + private genericEffect(): WeaponEffectInfo { + return { + execution: speed => this.genericExecutor(speed), + delay: ship => { + let result = (arenaDistance(this.source, this.destination) * 1.5) || 1; + if (this.action.blast) { + result += 300 * Phaser.Math.Easing.Quintic.Out(arenaDistance(this.destination, ship.location) / this.action.blast); + } + return result; + } + } + } + + /** + * Laser effect, scanning from one angle to the other + */ + laserExecutor(speed: number): Promise { + let duration = 1000 / speed; + let angle = arenaAngle(this.source, this.target); + let dangle = radians(this.action.angle) * 0.5; + + this.view.audio.playOnce("battle-weapon-laser", speed); + + let laser = this.builder.image("battle-effects-laser", this.source.x, this.source.y); + laser.setOrigin(0, 0.5); + laser.setRotation(angle - dangle); + laser.setScale(this.action.range / laser.width); + let dest_angle = laser.rotation + angularDifference(laser.rotation, angle + dangle); + return this.view.animations.addAnimation(laser, { rotation: dest_angle }, duration).then(() => laser.destroy()); + } + + private laserEffect(): WeaponEffectInfo { + return { + execution: speed => this.laserExecutor(speed), + delay: ship => { + let angle = arenaAngle(this.source, this.target); + let span = radians(this.action.angle); + return 900 * Math.abs(angularDifference(angle - span * 0.5, arenaAngle(this.source, ship.location))) / span; + } + } + } + + /** + * Submachine gun effect (quick chain of small bullets) + */ + bulletsExecutor(speed: number): Promise { + this.view.audio.playOnce("battle-weapon-bullets", speed); + + let target_ship = this.target.getShip(this.view.battle); + let has_shield = target_ship && (target_ship.getValue("shield") > 0); + + let angle = arenaAngle(this.source, this.target); + let distance = arenaDistance(this.source, this.target); + let guard = 35 + (has_shield ? 80 : 40); + if (guard + 1 > distance) { + guard = distance - 1; + } + let duration = 500 / speed; + let lifetime = (distance - guard) / (2 * speed); + this.builder.particles({ + key: "battle-effects-bullets", + source: { x: this.source.x + Math.cos(angle) * 35, y: this.source.y + Math.sin(angle) * 35, radius: 3 }, + emitDuration: duration, + count: 50, + lifetime: lifetime, + direction: { minangle: angle, maxangle: angle }, + scale: { min: 1, max: 1 }, + speed: { min: 2000 / speed, max: 2000 / speed }, + facing: ParticleFacingMode.ALWAYS + }); + + return this.timer.sleep(lifetime); + } + + private bulletsEffect(): WeaponEffectInfo { + return { + execution: speed => this.bulletsExecutor(speed), + delay: ship => 2000 / arenaDistance(this.source, ship.location) + } + } } diff --git a/src/ui/character/CharacterPersonality.ts b/src/ui/character/CharacterPersonality.ts index 7a59ca0..6d66714 100644 --- a/src/ui/character/CharacterPersonality.ts +++ b/src/ui/character/CharacterPersonality.ts @@ -1,67 +1,73 @@ -module TK.SpaceTac.UI { - /** - * Character personality traits editor - */ - export class CharacterPersonality { - private view: BaseView - private background: UIImage - private name: UIText - private ship?: Ship +import { bool } from "../../common/Tools"; +import { Ship } from "../../core/Ship"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIImage } from "../common/UIImage"; +import { UIText } from "../common/UIText"; +import { UITextDialog } from "../common/UITextDialog"; - constructor(builder: UIBuilder, x: number, y: number) { - this.view = builder.view; +/** + * Character personality traits editor + */ +export class CharacterPersonality { + private view: BaseView + private background: UIImage + private name: UIText + private ship?: Ship - this.background = builder.image("character-personality-background", x, y); - builder = builder.in(this.background); + constructor(builder: UIBuilder, x: number, y: number) { + this.view = builder.view; - builder.in(builder.image("character-section-title", 0, 0, false)).text("Pilot", 80, 45, { color: "#dce9f9", size: 32 }); + this.background = builder.image("character-personality-background", x, y); + builder = builder.in(this.background); - this.name = builder.in(builder.image("character-name-display", 430, 50, true)).text("", 0, 0, { size: 28 }); + builder.in(builder.image("character-section-title", 0, 0, false)).text("Pilot", 80, 45, { color: "#dce9f9", size: 32 }); - builder.button("character-name-button", 664, 0, () => this.renamePersonality(), "Rename personality"); + this.name = builder.in(builder.image("character-name-display", 430, 50, true)).text("", 0, 0, { size: 28 }); - builder.text("AVAILABLE SOON !", 690, 528, { size: 20, color: "#a7b3db" }); + builder.button("character-name-button", 664, 0, () => this.renamePersonality(), "Rename personality"); - builder.styled({ size: 24, color: "#dbeff9" }, builder => { - builder.image("character-personality-trait-base", 420, 198, true); - builder.text("Courageous", 144, 140); - builder.text("Wise", 725, 140); + builder.text("AVAILABLE SOON !", 690, 528, { size: 20, color: "#a7b3db" }); - builder.image("character-personality-trait-base", 420, 316, true); - builder.text("Kind", 144, 268); - builder.text("Resilient", 725, 268); + builder.styled({ size: 24, color: "#dbeff9" }, builder => { + builder.image("character-personality-trait-base", 420, 198, true); + builder.text("Courageous", 144, 140); + builder.text("Wise", 725, 140); - builder.image("character-personality-trait-base", 420, 444, true); - builder.text("Shrewd", 144, 388); - builder.text("Funny", 725, 388); - }); - } + builder.image("character-personality-trait-base", 420, 316, true); + builder.text("Kind", 144, 268); + builder.text("Resilient", 725, 268); - /** - * Change the content to display a ship's personality - */ - displayShip(ship: Ship) { - let builder = new UIBuilder(this.view); - this.ship = ship; + builder.image("character-personality-trait-base", 420, 444, true); + builder.text("Shrewd", 144, 388); + builder.text("Funny", 725, 388); + }); + } - builder.change(this.name, ship.name || ""); - } + /** + * Change the content to display a ship's personality + */ + displayShip(ship: Ship) { + let builder = new UIBuilder(this.view); + this.ship = ship; - /** - * Open a dialog to rename the ship's personality - */ - renamePersonality(): void { - if (!this.ship) { - return; - } - let ship = this.ship; + builder.change(this.name, ship.name || ""); + } - UITextDialog.ask(this.view, "Choose a name for this ship's personality", ship.name || undefined).then(name => { - if (bool(name)) { - ship.name = name; - this.displayShip(ship); - } - }); - } + /** + * Open a dialog to rename the ship's personality + */ + renamePersonality(): void { + if (!this.ship) { + return; } + let ship = this.ship; + + UITextDialog.ask(this.view, "Choose a name for this ship's personality", ship.name || undefined).then(name => { + if (bool(name)) { + ship.name = name; + this.displayShip(ship); + } + }); + } } diff --git a/src/ui/character/CharacterPortrait.ts b/src/ui/character/CharacterPortrait.ts index 7c0f5aa..a855bed 100644 --- a/src/ui/character/CharacterPortrait.ts +++ b/src/ui/character/CharacterPortrait.ts @@ -1,23 +1,26 @@ -module TK.SpaceTac.UI { - /** - * Display the portrait of a fleet member on a character sheet - */ - export class CharacterPortrait { - constructor(readonly ship: Ship) { - } +import { identity } from "../../common/Tools"; +import { Ship } from "../../core/Ship"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIButton } from "../common/UIButton"; - /** - * Draw the portrait (anchored at the center) - */ - draw(builder: UIBuilder, x: number, y: number, onselect: () => void): UIButton { - let button = builder.button("character-portrait", x, y, onselect, this.ship.getName(), identity, { center: true }); +/** + * Display the portrait of a fleet member on a character sheet + */ +export class CharacterPortrait { + constructor(readonly ship: Ship) { + } - builder.in(button, builder => { - let portrait = builder.image(`ship-${this.ship.model.code}-portrait`, 0, 0, true); - portrait.setScale(0.5); - }); + /** + * Draw the portrait (anchored at the center) + */ + draw(builder: UIBuilder, x: number, y: number, onselect: () => void): UIButton { + let button = builder.button("character-portrait", x, y, onselect, this.ship.getName(), identity, { center: true }); - return button; - } - } + builder.in(button, builder => { + let portrait = builder.image(`ship-${this.ship.model.code}-portrait`, 0, 0, true); + portrait.setScale(0.5); + }); + + return button; + } } diff --git a/src/ui/character/CharacterSheet.spec.ts b/src/ui/character/CharacterSheet.spec.ts index d2e7eda..44f2c6e 100644 --- a/src/ui/character/CharacterSheet.spec.ts +++ b/src/ui/character/CharacterSheet.spec.ts @@ -1,56 +1,62 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterSheet", test => { +import { testing } from "../../common/Testing"; +import { as } from "../../common/Tools"; +import { Fleet } from "../../core/Fleet"; +import { Ship } from "../../core/Ship"; +import { UIButton } from "../common/UIButton"; +import { setupEmptyView } from "../TestGame"; +import { CharacterSheet, CharacterSheetMode } from "./CharacterSheet"; - testing("in UI", test => { - let testgame = setupEmptyView(test); +testing("CharacterSheet", test => { - test.case("displays fleet and ship information", check => { - let view = testgame.view; - check.patch(view, "getWidth", () => 1240); - let sheet = new CharacterSheet(view, CharacterSheetMode.DISPLAY); + testing("in UI", test => { + let testgame = setupEmptyView(test); - check.equals(sheet.container.x, -1240); + test.case("displays fleet and ship information", check => { + let view = testgame.view; + check.patch(view, "getWidth", () => 1240); + let sheet = new CharacterSheet(view, CharacterSheetMode.DISPLAY); - let fleet = new Fleet(); - let ship1 = fleet.addShip(); - ship1.name = "Ship 1"; - let ship2 = fleet.addShip(); - ship2.name = "Ship 2"; + check.equals(sheet.container.x, -1240); - sheet.show(ship1, false); + let fleet = new Fleet(); + let ship1 = fleet.addShip(); + ship1.name = "Ship 1"; + let ship2 = fleet.addShip(); + ship2.name = "Ship 2"; - check.equals(sheet.container.x, 0); - check.equals(sheet.group_portraits.length, 2); + sheet.show(ship1, false); - check.equals(sheet.text_name && sheet.text_name.text, "Ship 1"); + check.equals(sheet.container.x, 0); + check.equals(sheet.group_portraits.length, 2); - let portrait = as(UIButton, sheet.group_portraits.getAt(1)); - portrait.emit("pointerdown", { buttons: 1 }); - portrait.emit("pointerup", { buttons: 1 }); + check.equals(sheet.text_name && sheet.text_name.text, "Ship 1"); - check.equals(sheet.text_name && sheet.text_name.text, "Ship 2"); - }); + let portrait = as(UIButton, sheet.group_portraits.getAt(1)); + portrait.emit("pointerdown", { buttons: 1 }); + portrait.emit("pointerup", { buttons: 1 }); - test.case("controls global interactivity state", check => { - let sheet = new CharacterSheet(testgame.view, CharacterSheetMode.EDITION); - check.equals(sheet.isInteractive(), false, "no ship"); - - let ship = new Ship(); - ship.critical = true; - sheet.show(ship); - check.equals(sheet.isInteractive(), false, "critical ship"); - - ship.critical = false; - sheet.show(ship); - check.equals(sheet.isInteractive(), true, "normal ship"); - - sheet = new CharacterSheet(testgame.view, CharacterSheetMode.DISPLAY); - sheet.show(ship); - check.equals(sheet.isInteractive(), false, "interactivity disabled"); - - sheet.show(ship); - check.equals(sheet.isInteractive(), false, "interactivity stays disabled"); - }); - }); + check.equals(sheet.text_name && sheet.text_name.text, "Ship 2"); }); -} + + test.case("controls global interactivity state", check => { + let sheet = new CharacterSheet(testgame.view, CharacterSheetMode.EDITION); + check.equals(sheet.isInteractive(), false, "no ship"); + + let ship = new Ship(); + ship.critical = true; + sheet.show(ship); + check.equals(sheet.isInteractive(), false, "critical ship"); + + ship.critical = false; + sheet.show(ship); + check.equals(sheet.isInteractive(), true, "normal ship"); + + sheet = new CharacterSheet(testgame.view, CharacterSheetMode.DISPLAY); + sheet.show(ship); + check.equals(sheet.isInteractive(), false, "interactivity disabled"); + + sheet.show(ship); + check.equals(sheet.isInteractive(), false, "interactivity stays disabled"); + }); + }); +}); diff --git a/src/ui/character/CharacterSheet.ts b/src/ui/character/CharacterSheet.ts index 3219e4d..d05990d 100644 --- a/src/ui/character/CharacterSheet.ts +++ b/src/ui/character/CharacterSheet.ts @@ -1,409 +1,427 @@ -module TK.SpaceTac.UI { - export enum CharacterSheetMode { - CREATION, - EDITION, - DISPLAY +import { capitalize, cfilter, keys, range } from "../../common/Tools" +import { EndTurnAction } from "../../core/actions/EndTurnAction" +import { AttributeEffect } from "../../core/effects/AttributeEffect" +import { Fleet } from "../../core/Fleet" +import { ShipModel } from "../../core/models/ShipModel" +import { Ship } from "../../core/Ship" +import { SHIP_ATTRIBUTES, SHIP_VALUES_DESCRIPTIONS, SHIP_VALUES_NAMES } from "../../core/ShipValue" +import { BaseView } from "../BaseView" +import { ActionTooltip } from "../battle/ActionTooltip" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton, UIButtonUnicity } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { UIText } from "../common/UIText" +import { destroyChildren } from "../common/UITools" +import { ValueBar } from "../common/ValueBar" +import { CharacterPersonality } from "./CharacterPersonality" +import { CharacterPortrait } from "./CharacterPortrait" +import { CharacterUpgrade } from "./CharacterUpgrade" + +export enum CharacterSheetMode { + CREATION, + EDITION, + DISPLAY +} + +/** + * Character sheet, displaying ship characteristics + */ +export class CharacterSheet { + // Global sheet mode + mode: CharacterSheetMode + + // Parent view + view: BaseView + + // UI components builder + container: UIContainer + builder: UIBuilder + + // Close/validate button + close_button: UIButton + + // X positions + xshown = 0 + xhidden = -2000 + + // Groups + group_level: UIContainer + group_portraits: UIContainer + group_attributes: UIContainer + group_actions: UIContainer + group_upgrades: UIContainer + + // Currently displayed fleet + fleet?: Fleet + + // Currently displayed ship + ship?: Ship + + // Variable data + personality?: CharacterPersonality + image_portrait: UIImage + text_model: UIText + text_description: UIText + text_name?: UIText + text_level: UIText + text_upgrade_points: UIText + valuebar_experience: ValueBar + + constructor(view: BaseView, mode: CharacterSheetMode, onclose?: Function) { + this.view = view; + this.mode = mode; + + let builder = new UIBuilder(view); + this.container = builder.container("character-sheet"); + + builder = builder.in(this.container); + let bg = builder.image("character-sheet"); + bg.setInteractive(); + + this.builder = builder.styled({ color: "#dce9f9", size: 16, shadow: true }); + + if (!onclose) { + onclose = () => this.hide(); } - /** - * Character sheet, displaying ship characteristics - */ - export class CharacterSheet { - // Global sheet mode - mode: CharacterSheetMode + this.xhidden = -this.view.getWidth(); + this.container.x = this.xhidden; + this.container.setVisible(false); - // Parent view - view: BaseView + this.image_portrait = this.builder.image("common-transparent", 435, 271, true); - // UI components builder - container: UIContainer - builder: UIBuilder + this.builder.image("character-entry", 24, 740); - // Close/validate button - close_button: UIButton + this.group_portraits = this.builder.container("portraits", 90, 755); - // X positions - xshown = 0 - xhidden = -2000 + let model_bg = this.builder.image("character-ship-model", 434, 500, true); + this.text_model = this.builder.in(model_bg).text("", 0, 0, { size: 28 }); - // Groups - group_level: UIContainer - group_portraits: UIContainer - group_attributes: UIContainer - group_actions: UIContainer - group_upgrades: UIContainer + let description_bg = this.builder.image("character-ship-description", 434, 654, true); + this.text_description = this.builder.in(description_bg).text("", 0, 0, { color: "#a0afc3", width: 510 }); - // Currently displayed fleet - fleet?: Fleet + this.group_attributes = this.builder.container("attributes", 28, 28); + this.group_actions = this.builder.container("actions", 698, 28); - // Currently displayed ship - ship?: Ship + this.group_level = this.builder.container("level"); + let points_bg = this.builder.in(this.group_level).image("character-level-upgrades", 582, 986); + this.builder.in(points_bg, builder => { + builder.text("Upgrade points", 46, 10, { center: false, vcenter: false }); + builder.image("character-upgrade-point", 147, 59, true); + }); + this.text_upgrade_points = this.builder.in(points_bg).text("", 106, 60, { size: 28 }); - // Variable data - personality?: CharacterPersonality - image_portrait: UIImage - text_model: UIText - text_description: UIText - text_name?: UIText - text_level: UIText - text_upgrade_points: UIText - valuebar_experience: ValueBar + let level_bg = this.builder.in(this.group_level).image("character-level-display", 434, 1032, true); + this.text_level = this.builder.in(level_bg).text("", 0, 4, { size: 28 }); + this.valuebar_experience = this.builder.in(level_bg).valuebar("character-level-experience", -level_bg.width * 0.5, -level_bg.height * 0.5); - constructor(view: BaseView, mode: CharacterSheetMode, onclose?: Function) { - this.view = view; - this.mode = mode; + this.group_upgrades = this.builder.container("upgrades"); - let builder = new UIBuilder(view); - this.container = builder.container("character-sheet"); + if (this.mode == CharacterSheetMode.CREATION) { + this.builder.in(this.builder.image("character-section-title", 180, 30, false)).text("Ship", 80, 45, { color: "#dce9f9", size: 32 }); - builder = builder.in(this.container); - let bg = builder.image("character-sheet"); - bg.setInteractive(); + this.personality = new CharacterPersonality(this.builder, 950, 30); - this.builder = builder.styled({ color: "#dce9f9", size: 16, shadow: true }); + this.close_button = this.builder.button("character-validate-creation", 140, 930, onclose, + "Validate the team, and start the campaign", undefined, { + hover_bottom: true, + text: "Validate team", + text_x: 295, + text_y: 57, + text_style: { size: 32, color: "#fff3df" } + } + ); - if (!onclose) { - onclose = () => this.hide(); + this.builder.in(this.builder.image("character-creation-help", 970, 680), builder => { + builder.text("Compose your initial team by choosing a model for each ship, and customize the name and personality of the Artificial Intelligence pilot", + 405, 150, { color: "#a3bbd9", size: 22, width: 500 }); + }); + + this.builder.button("character-model-prev", 216, 500, () => this.changeModel(-1), "Select previous model", undefined, { center: true }); + this.builder.button("character-model-next", 654, 500, () => this.changeModel(1), "Select next model", undefined, { center: true }); + + this.group_level.visible = false; + this.group_upgrades.visible = false; + } else { + this.text_name = this.builder.in(this.builder.image("character-name-display", 434, 940, true)).text("", 0, 0, { size: 28 }); + + this.close_button = this.builder.button("character-close-button", 1837, 0, onclose, "Close the character sheet"); + } + + this.refreshUpgrades(); + this.refreshAttributes(); + this.refreshActions(); + } + + /** + * Move the sheet to a specific layer + */ + moveToLayer(layer: UIContainer): void { + layer.add(this.container); + } + + /** + * Check if the sheet should be interactive + */ + isInteractive(): boolean { + return this.ship ? (this.mode != CharacterSheetMode.DISPLAY && !this.ship.critical) : false; + } + + /** + * Change the ship model + */ + changeModel(offset: number): void { + if (this.mode == CharacterSheetMode.CREATION && this.ship) { + let models = ShipModel.getDefaultCollection(); + + let idx = models.map(model => model.code).indexOf(this.ship.model.code) + offset; + if (idx < 0) { + idx = models.length - 1; + } else if (idx >= models.length) { + idx = 0; + } + + this.ship.setModel(models[idx]); + this.refresh(); + } + } + + /** + * Refresh the ship information display + */ + private refreshShipInfo(): void { + if (this.ship) { + let ship = this.ship; + this.builder.change(this.image_portrait, `ship-${ship.model.code}-portrait`); + if (this.text_name) { + this.text_name.setText(ship.name || ""); + } + if (this.personality) { + this.personality.displayShip(ship); + } + this.text_model.setText(ship.model.name); + this.text_level.setText(`Level ${ship.level.get()}`); + this.text_description.setText(ship.model.getDescription()); + this.text_upgrade_points.setText(`${ship.getAvailableUpgradePoints()}`); + this.valuebar_experience.setValue(ship.level.getExperience(), ship.level.getNextGoal()); + } + } + + /** + * Refresh the upgrades display + */ + private refreshUpgrades(): void { + let builder = this.builder.in(this.group_upgrades); + builder.clear(); + + if (!this.ship || this.mode == CharacterSheetMode.CREATION) { + return; + } + let ship = this.ship; + + let initial = builder.image("character-initial", 970, 30); + + // Base equipment (level 1) + builder.styled({ center: false, vcenter: false }).in(initial, builder => { + builder.text("Base equipment", 32, 8, { color: "#e2e9d1" }); + + builder.in(builder.container("attributes"), builder => { + let effects = cfilter(ship.model.getEffects(1, []), AttributeEffect); + effects.forEach(effect => { + let button = builder.button(`attribute-${effect.attrcode}`, 0, 8, undefined, + `${capitalize(SHIP_VALUES_NAMES[effect.attrcode])} - ${SHIP_VALUES_DESCRIPTIONS[effect.attrcode]}`); + + builder.in(button, builder => { + builder.text(`${effect.value}`, 56, 8, { size: 22 }); + }); + }); + builder.distribute("x", 236, 870); + }); + + builder.in(builder.container("actions"), builder => { + let actions = ship.model.getActions(1, []); + actions.forEach(action => { + let button = builder.button("common-transparent", 0, 66, undefined, action.getEffectsDescription()); + + builder.in(button, builder => { + let icon = builder.image(`action-${action.code}`); + icon.setScale(0.1875); + if (actions.length < 5) { + builder.text(`${action.name}`, 56, 12, { size: 16 }); } + }); + }); + builder.distribute("x", 28, 888); + }); + }); - this.xhidden = -this.view.getWidth(); - this.container.x = this.xhidden; - this.container.setVisible(false); + // Level number + range(10).forEach(i => { + builder.text(`${i + 1}`, 920, i == 0 ? 92 : (110 + i * 100), { + center: true, + vcenter: true, + size: 28, + color: ship.level.get() >= (i + 1) ? "#dce9f9" : "#293038" + }); + }); - this.image_portrait = this.builder.image("common-transparent", 435, 271, true); + // Level upgrades + range(9).forEach(i => { + builder.image("character-level-separator", 844, 154 + i * 100); - this.builder.image("character-entry", 24, 740); + let level = i + 2; + let upgrades = ship.model.getLevelUpgrades(level); + upgrades.forEach((upgrade, j) => { + let onchange = (selected: boolean) => { + this.refreshShipInfo(); // TODO Only upgrade points + this.refreshActions(); + this.refreshAttributes(); + }; + new CharacterUpgrade(ship, upgrade, level).draw(builder, 970 + j * 315, 170 + i * 100, + this.isInteractive() ? onchange : undefined); + }); + }); + } - this.group_portraits = this.builder.container("portraits", 90, 755); + /** + * Refresh the attributes display + */ + private refreshAttributes(): void { + let builder = this.builder.in(this.group_attributes); + builder.clear(); - let model_bg = this.builder.image("character-ship-model", 434, 500, true); - this.text_model = this.builder.in(model_bg).text("", 0, 0, { size: 28 }); + builder.image("character-ship-column-left", 0, 0); - let description_bg = this.builder.image("character-ship-description", 434, 654, true); - this.text_description = this.builder.in(description_bg).text("", 0, 0, { color: "#a0afc3", width: 510 }); + builder.text("Attributes", 74, 20, { color: "#a3bbd9" }); - this.group_attributes = this.builder.container("attributes", 28, 28); - this.group_actions = this.builder.container("actions", 698, 28); + if (this.ship) { + let ship = this.ship; + builder.in(builder.container("items"), builder => { + keys(SHIP_ATTRIBUTES).forEach(attribute => { + let button = builder.button(`attribute-${attribute}`, 24, 0, undefined, + ship.getAttributeDescription(attribute)); - this.group_level = this.builder.container("level"); - let points_bg = this.builder.in(this.group_level).image("character-level-upgrades", 582, 986); - this.builder.in(points_bg, builder => { - builder.text("Upgrade points", 46, 10, { center: false, vcenter: false }); - builder.image("character-upgrade-point", 147, 59, true); - }); - this.text_upgrade_points = this.builder.in(points_bg).text("", 106, 60, { size: 28 }); + builder.in(button).text(`${ship.getAttribute(attribute)}`, 78, 27, { size: 22 }); + }); + builder.distribute("y", 40, 688); + }); + } + } - let level_bg = this.builder.in(this.group_level).image("character-level-display", 434, 1032, true); - this.text_level = this.builder.in(level_bg).text("", 0, 4, { size: 28 }); - this.valuebar_experience = this.builder.in(level_bg).valuebar("character-level-experience", -level_bg.width * 0.5, -level_bg.height * 0.5); + /** + * Refresh the actions display + */ + private refreshActions(): void { + let builder = this.builder.in(this.group_actions); + builder.clear(); - this.group_upgrades = this.builder.container("upgrades"); + builder.image("character-ship-column-right", 0, 0); - if (this.mode == CharacterSheetMode.CREATION) { - this.builder.in(this.builder.image("character-section-title", 180, 30, false)).text("Ship", 80, 45, { color: "#dce9f9", size: 32 }); + builder.text("Actions", 74, 20, { color: "#a3bbd9" }); - this.personality = new CharacterPersonality(this.builder, 950, 30); + if (this.ship) { + let ship = this.ship; + builder.in(builder.container("items"), builder => { + let actions = ship.actions.listAll().filter(action => !(action instanceof EndTurnAction)); + actions.forEach(action => { + let button = builder.button(`action-${action.code}`, 24, 0, undefined, filler => ActionTooltip.fill(filler, ship, action)); + button.setScale(0.375); + }); + builder.distribute("y", 40, 688); + }); + } + } - this.close_button = this.builder.button("character-validate-creation", 140, 930, onclose, - "Validate the team, and start the campaign", undefined, { - hover_bottom: true, - text: "Validate team", - text_x: 295, - text_y: 57, - text_style: { size: 32, color: "#fff3df" } - } - ); + /** + * Refresh the fleet display + */ + private refreshFleet(): void { + destroyChildren(this.group_portraits); - this.builder.in(this.builder.image("character-creation-help", 970, 680), builder => { - builder.text("Compose your initial team by choosing a model for each ship, and customize the name and personality of the Artificial Intelligence pilot", - 405, 150, { color: "#a3bbd9", size: 22, width: 500 }); - }); - - this.builder.button("character-model-prev", 216, 500, () => this.changeModel(-1), "Select previous model", undefined, { center: true }); - this.builder.button("character-model-next", 654, 500, () => this.changeModel(1), "Select next model", undefined, { center: true }); - - this.group_level.visible = false; - this.group_upgrades.visible = false; - } else { - this.text_name = this.builder.in(this.builder.image("character-name-display", 434, 940, true)).text("", 0, 0, { size: 28 }); - - this.close_button = this.builder.button("character-close-button", 1837, 0, onclose, "Close the character sheet"); - } - - this.refreshUpgrades(); - this.refreshAttributes(); - this.refreshActions(); - } - - /** - * Move the sheet to a specific layer - */ - moveToLayer(layer: UIContainer): void { - layer.add(this.container); - } - - /** - * Check if the sheet should be interactive - */ - isInteractive(): boolean { - return this.ship ? (this.mode != CharacterSheetMode.DISPLAY && !this.ship.critical) : false; - } - - /** - * Change the ship model - */ - changeModel(offset: number): void { - if (this.mode == CharacterSheetMode.CREATION && this.ship) { - let models = ShipModel.getDefaultCollection(); - - let idx = models.map(model => model.code).indexOf(this.ship.model.code) + offset; - if (idx < 0) { - idx = models.length - 1; - } else if (idx >= models.length) { - idx = 0; - } - - this.ship.setModel(models[idx]); - this.refresh(); - } - } - - /** - * Refresh the ship information display - */ - private refreshShipInfo(): void { - if (this.ship) { - let ship = this.ship; - this.builder.change(this.image_portrait, `ship-${ship.model.code}-portrait`); - if (this.text_name) { - this.text_name.setText(ship.name || ""); - } - if (this.personality) { - this.personality.displayShip(ship); - } - this.text_model.setText(ship.model.name); - this.text_level.setText(`Level ${ship.level.get()}`); - this.text_description.setText(ship.model.getDescription()); - this.text_upgrade_points.setText(`${ship.getAvailableUpgradePoints()}`); - this.valuebar_experience.setValue(ship.level.getExperience(), ship.level.getNextGoal()); - } - } - - /** - * Refresh the upgrades display - */ - private refreshUpgrades(): void { - let builder = this.builder.in(this.group_upgrades); - builder.clear(); - - if (!this.ship || this.mode == CharacterSheetMode.CREATION) { - return; - } - let ship = this.ship; - - let initial = builder.image("character-initial", 970, 30); - - // Base equipment (level 1) - builder.styled({ center: false, vcenter: false }).in(initial, builder => { - builder.text("Base equipment", 32, 8, { color: "#e2e9d1" }); - - builder.in(builder.container("attributes"), builder => { - let effects = cfilter(ship.model.getEffects(1, []), AttributeEffect); - effects.forEach(effect => { - let button = builder.button(`attribute-${effect.attrcode}`, 0, 8, undefined, - `${capitalize(SHIP_VALUES_NAMES[effect.attrcode])} - ${SHIP_VALUES_DESCRIPTIONS[effect.attrcode]}`); - - builder.in(button, builder => { - builder.text(`${effect.value}`, 56, 8, { size: 22 }); - }); - }); - builder.distribute("x", 236, 870); - }); - - builder.in(builder.container("actions"), builder => { - let actions = ship.model.getActions(1, []); - actions.forEach(action => { - let button = builder.button("common-transparent", 0, 66, undefined, action.getEffectsDescription()); - - builder.in(button, builder => { - let icon = builder.image(`action-${action.code}`); - icon.setScale(0.1875); - if (actions.length < 5) { - builder.text(`${action.name}`, 56, 12, { size: 16 }); - } - }); - }); - builder.distribute("x", 28, 888); - }); - }); - - // Level number - range(10).forEach(i => { - builder.text(`${i + 1}`, 920, i == 0 ? 92 : (110 + i * 100), { - center: true, - vcenter: true, - size: 28, - color: ship.level.get() >= (i + 1) ? "#dce9f9" : "#293038" - }); - }); - - // Level upgrades - range(9).forEach(i => { - builder.image("character-level-separator", 844, 154 + i * 100); - - let level = i + 2; - let upgrades = ship.model.getLevelUpgrades(level); - upgrades.forEach((upgrade, j) => { - let onchange = (selected: boolean) => { - this.refreshShipInfo(); // TODO Only upgrade points - this.refreshActions(); - this.refreshAttributes(); - }; - new CharacterUpgrade(ship, upgrade, level).draw(builder, 970 + j * 315, 170 + i * 100, - this.isInteractive() ? onchange : undefined); - }); - }); - } - - /** - * Refresh the attributes display - */ - private refreshAttributes(): void { - let builder = this.builder.in(this.group_attributes); - builder.clear(); - - builder.image("character-ship-column-left", 0, 0); - - builder.text("Attributes", 74, 20, { color: "#a3bbd9" }); - - if (this.ship) { - let ship = this.ship; - builder.in(builder.container("items"), builder => { - keys(SHIP_ATTRIBUTES).forEach(attribute => { - let button = builder.button(`attribute-${attribute}`, 24, 0, undefined, - ship.getAttributeDescription(attribute)); - - builder.in(button).text(`${ship.getAttribute(attribute)}`, 78, 27, { size: 22 }); - }); - builder.distribute("y", 40, 688); - }); - } - } - - /** - * Refresh the actions display - */ - private refreshActions(): void { - let builder = this.builder.in(this.group_actions); - builder.clear(); - - builder.image("character-ship-column-right", 0, 0); - - builder.text("Actions", 74, 20, { color: "#a3bbd9" }); - - if (this.ship) { - let ship = this.ship; - builder.in(builder.container("items"), builder => { - let actions = ship.actions.listAll().filter(action => !(action instanceof EndTurnAction)); - actions.forEach(action => { - let button = builder.button(`action-${action.code}`, 24, 0, undefined, filler => ActionTooltip.fill(filler, ship, action)); - button.setScale(0.375); - }); - builder.distribute("y", 40, 688); - }); - } - } - - /** - * Refresh the fleet display - */ - private refreshFleet(): void { - destroyChildren(this.group_portraits); - - if (this.fleet) { - let builder = this.builder.in(this.group_portraits); - this.fleet.ships.forEach((ship, idx) => { - let button: UIButton; - button = new CharacterPortrait(ship).draw(builder, 64 + idx * 140, 64, () => { - if (button) { - button.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN); - this.ship = ship; - - this.refreshShipInfo(); - this.refreshActions(); - this.refreshAttributes(); - this.refreshUpgrades(); - } - }); - - if (ship == this.ship) { - button.toggle(true); - } - }); - } - } - - /** - * Check if the sheet is shown - */ - isOpened(): boolean { - return this.container.x != this.xhidden; - } - - /** - * Show the sheet for a given ship - */ - show(ship: Ship, animate = true, sound = true) { + if (this.fleet) { + let builder = this.builder.in(this.group_portraits); + this.fleet.ships.forEach((ship, idx) => { + let button: UIButton; + button = new CharacterPortrait(ship).draw(builder, 64 + idx * 140, 64, () => { + if (button) { + button.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN); this.ship = ship; - this.fleet = ship.fleet; this.refreshShipInfo(); - this.refreshUpgrades(); - this.refreshAttributes(); this.refreshActions(); + this.refreshAttributes(); + this.refreshUpgrades(); + } + }); - if (ship.fleet !== this.fleet || ship.fleet.ships.length != this.group_portraits.length) { - this.refreshFleet(); - } - - if (sound) { - this.view.audio.playOnce("ui-dialog-open"); - } - - this.container.setVisible(true); - if (animate) { - this.view.animations.addAnimation(this.container, { x: this.xshown }, 400, "Circ.easeOut"); - } else { - this.container.x = this.xshown; - } - } - - /** - * Hide the sheet - */ - async hide(animate = true) { - this.view.audio.playOnce("ui-dialog-close"); - - if (animate) { - await this.view.animations.addAnimation(this.container, { x: this.xhidden }, 400, "Circ.easeIn"); - } else { - this.container.x = this.xhidden; - } - this.container.setVisible(false); - } - - /** - * Refresh the sheet display - */ - refresh() { - if (this.ship) { - this.refreshShipInfo(); - this.refreshUpgrades(); - this.refreshAttributes(); - this.refreshActions(); - - this.refreshFleet(); - } + if (ship == this.ship) { + button.toggle(true); } + }); } + } + + /** + * Check if the sheet is shown + */ + isOpened(): boolean { + return this.container.x != this.xhidden; + } + + /** + * Show the sheet for a given ship + */ + show(ship: Ship, animate = true, sound = true) { + this.ship = ship; + this.fleet = ship.fleet; + + this.refreshShipInfo(); + this.refreshUpgrades(); + this.refreshAttributes(); + this.refreshActions(); + + if (ship.fleet !== this.fleet || ship.fleet.ships.length != this.group_portraits.length) { + this.refreshFleet(); + } + + if (sound) { + this.view.audio.playOnce("ui-dialog-open"); + } + + this.container.setVisible(true); + if (animate) { + this.view.animations.addAnimation(this.container, { x: this.xshown }, 400, "Circ.easeOut"); + } else { + this.container.x = this.xshown; + } + } + + /** + * Hide the sheet + */ + async hide(animate = true) { + this.view.audio.playOnce("ui-dialog-close"); + + if (animate) { + await this.view.animations.addAnimation(this.container, { x: this.xhidden }, 400, "Circ.easeIn"); + } else { + this.container.x = this.xhidden; + } + this.container.setVisible(false); + } + + /** + * Refresh the sheet display + */ + refresh() { + if (this.ship) { + this.refreshShipInfo(); + this.refreshUpgrades(); + this.refreshAttributes(); + this.refreshActions(); + + this.refreshFleet(); + } + } } diff --git a/src/ui/character/CharacterUpgrade.spec.ts b/src/ui/character/CharacterUpgrade.spec.ts index 0401ce9..dbb6853 100644 --- a/src/ui/character/CharacterUpgrade.spec.ts +++ b/src/ui/character/CharacterUpgrade.spec.ts @@ -1,73 +1,83 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterUpgrade", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { BaseAction } from "../../core/actions/BaseAction"; +import { TriggerAction } from "../../core/actions/TriggerAction"; +import { AttributeEffect } from "../../core/effects/AttributeEffect"; +import { AttributeMultiplyEffect } from "../../core/effects/AttributeMultiplyEffect"; +import { DamageEffect } from "../../core/effects/DamageEffect"; +import { ShipUpgrade } from "../../core/models/ShipModel"; +import { Ship } from "../../core/Ship"; +import { TooltipBuilder, TooltipContainer } from "../common/Tooltip"; +import { collectTexts, setupEmptyView } from "../TestGame"; +import { CharacterUpgrade } from "./CharacterUpgrade"; - test.acase("fills tooltip content", async check => { - let ship = new Ship(); - let upgrade: ShipUpgrade = { - code: "Test Upgrade", - description: "A super ship upgrade", - effects: [ - new AttributeEffect("hull_capacity", 10), - new AttributeEffect("shield_capacity", 5), - ] - }; +testing("CharacterUpgrade", test => { + let testgame = setupEmptyView(test); - let display = new CharacterUpgrade(ship, upgrade, 3); - let tooltip = new TooltipContainer(testgame.view); - let builder = new TooltipBuilder(tooltip); - display.fillTooltip(builder); - check.equals(collectTexts(tooltip.content), [ - "Test Upgrade", - "Permanent effects:", - "• hull capacity +10", - "• shield capacity +5", - "A super ship upgrade", - ]); + test.acase("fills tooltip content", async check => { + let ship = new Ship(); + let upgrade: ShipUpgrade = { + code: "Test Upgrade", + description: "A super ship upgrade", + effects: [ + new AttributeEffect("hull_capacity", 10), + new AttributeEffect("shield_capacity", 5), + ] + }; - upgrade.effects = []; - upgrade.actions = [new TriggerAction("Test action", { - range: 50, - effects: [new DamageEffect(10)] - })]; + let display = new CharacterUpgrade(ship, upgrade, 3); + let tooltip = new TooltipContainer(testgame.view); + let builder = new TooltipBuilder(tooltip); + display.fillTooltip(builder); + check.equals(collectTexts(tooltip.content), [ + "Test Upgrade", + "Permanent effects:", + "• hull capacity +10", + "• shield capacity +5", + "A super ship upgrade", + ]); - builder.clear(); - display.fillTooltip(builder); - check.equals(collectTexts(tooltip.content), [ - "Test Upgrade", - "Fire (power 1, range 50km):", - "• do 10 damage on target", - "A super ship upgrade", - ]); - }) + upgrade.effects = []; + upgrade.actions = [new TriggerAction("Test action", { + range: 50, + effects: [new DamageEffect(10)] + })]; - test.acase("gets an appropriate icon", async check => { - let ship = new Ship(); - let upgrade: ShipUpgrade = { - code: "Test Upgrade", - effects: [ - new AttributeEffect("hull_capacity", 10), - new AttributeEffect("shield_capacity", 5), - ] - }; - let display = new CharacterUpgrade(ship, upgrade, 3); - check.equals(display.getIcon(), "attribute-hull_capacity"); + builder.clear(); + display.fillTooltip(builder); + check.equals(collectTexts(tooltip.content), [ + "Test Upgrade", + "Fire (power 1, range 50km):", + "• do 10 damage on target", + "A super ship upgrade", + ]); + }) - upgrade.effects = []; - upgrade.actions = [new BaseAction("Test Action")]; - check.equals(display.getIcon(), "action-testaction"); + test.acase("gets an appropriate icon", async check => { + let ship = new Ship(); + let upgrade: ShipUpgrade = { + code: "Test Upgrade", + effects: [ + new AttributeEffect("hull_capacity", 10), + new AttributeEffect("shield_capacity", 5), + ] + }; + let display = new CharacterUpgrade(ship, upgrade, 3); + check.equals(display.getIcon(), "attribute-hull_capacity"); - upgrade.actions = []; - check.equals(display.getIcon(), "common-transparent"); + upgrade.effects = []; + upgrade.actions = [new BaseAction("Test Action")]; + check.equals(display.getIcon(), "action-testaction"); - upgrade.effects = [new DamageEffect(10)]; - check.equals(display.getIcon(), "common-transparent"); + upgrade.actions = []; + check.equals(display.getIcon(), "common-transparent"); - upgrade.effects = [ - new DamageEffect(10), - new AttributeMultiplyEffect("evasion", 2) - ]; - check.equals(display.getIcon(), "attribute-evasion"); - }) - }) -} + upgrade.effects = [new DamageEffect(10)]; + check.equals(display.getIcon(), "common-transparent"); + + upgrade.effects = [ + new DamageEffect(10), + new AttributeMultiplyEffect("evasion", 2) + ]; + check.equals(display.getIcon(), "attribute-evasion"); + }) +}) diff --git a/src/ui/character/CharacterUpgrade.ts b/src/ui/character/CharacterUpgrade.ts index 140d95b..ec90d3f 100644 --- a/src/ui/character/CharacterUpgrade.ts +++ b/src/ui/character/CharacterUpgrade.ts @@ -1,115 +1,121 @@ -module TK.SpaceTac.UI { - /** - * Display a single upgrade options - */ - export class CharacterUpgrade { - constructor( - readonly ship: Ship, - readonly upgrade: ShipUpgrade, - readonly level: number - ) { - } +import { bool, first, range } from "../../common/Tools"; +import { AttributeEffect } from "../../core/effects/AttributeEffect"; +import { AttributeMultiplyEffect } from "../../core/effects/AttributeMultiplyEffect"; +import { ShipUpgrade } from "../../core/models/ShipModel"; +import { Ship } from "../../core/Ship"; +import { TooltipBuilder } from "../common/Tooltip"; +import { UIBuilder } from "../common/UIBuilder"; - /** - * Draw the upgrade button - */ - draw(builder: UIBuilder, x: number, y: number, onchange?: (selected: boolean) => void): void { - let active = this.ship.level.hasUpgrade(this.upgrade); - let enabled = onchange ? (this.ship.level.get() >= this.level) : active; - let tooltip = enabled ? ((filler: TooltipBuilder) => this.fillTooltip(filler)) : undefined; - let selector = (enabled && onchange) ? ((on: boolean) => this.activate(on, onchange)) : undefined; - let button = builder.button("character-upgrade", x, y, undefined, tooltip, selector); +/** + * Display a single upgrade options + */ +export class CharacterUpgrade { + constructor( + readonly ship: Ship, + readonly upgrade: ShipUpgrade, + readonly level: number + ) { + } - if (active) { - button.toggle(true); - } + /** + * Draw the upgrade button + */ + draw(builder: UIBuilder, x: number, y: number, onchange?: (selected: boolean) => void): void { + let active = this.ship.level.hasUpgrade(this.upgrade); + let enabled = onchange ? (this.ship.level.get() >= this.level) : active; + let tooltip = enabled ? ((filler: TooltipBuilder) => this.fillTooltip(filler)) : undefined; + let selector = (enabled && onchange) ? ((on: boolean) => this.activate(on, onchange)) : undefined; + let button = builder.button("character-upgrade", x, y, undefined, tooltip, selector); - builder.in(button, builder => { - if (enabled) { - builder.text(this.upgrade.code, 166, 40, { size: 16, color: "#e7ebf0", width: 210 }); - - let icon = builder.image(this.getIcon(), 40, 40, true); - if (icon.width && icon.width > 64) { - icon.setScale(64 / icon.width); - } - - range(this.upgrade.cost || 0).forEach(i => { - builder.image("character-upgrade-point", 275, 64 - i * 24, true); - }); - } else { - builder.image("character-upgrade-locked"); - } - }); - } - - /** - * Activate or deactivate the upgrade - */ - private activate(on: boolean, onchange: (selected: boolean) => void): boolean { - let oldval = this.ship.level.hasUpgrade(this.upgrade); - this.ship.activateUpgrade(this.upgrade, on); - let newval = this.ship.level.hasUpgrade(this.upgrade); - - if (newval != oldval) { - onchange(newval); - } - - return newval; - } - - /** - * Fill the tooltip for this upgrade - */ - fillTooltip(builder: TooltipBuilder): boolean { - builder.text(this.upgrade.code, 0, 0, { size: 20 }); - - let y = 42; - - if (bool(this.upgrade.effects)) { - builder.text("Permanent effects:", 0, y); - y += 30; - this.upgrade.effects.forEach(effect => { - builder.text(`• ${effect.getDescription()}`, 0, y); - y += 30; - }); - } - - if (bool(this.upgrade.actions)) { - this.upgrade.actions.forEach(action => { - action.getEffectsDescription().split('\n').forEach(line => { - builder.text(line, 0, y); - y += 30; - }) - }); - } - - if (bool(this.upgrade.description)) { - y += 10; - builder.text(this.upgrade.description, 0, y, { size: 14, color: "#999999", width: 540 }); - } - - return true; - } - - /** - * Get an icon code for an upgrade - */ - getIcon(): string { - let upgrade = this.upgrade; - - if (upgrade.actions && upgrade.actions.length) { - return `action-${upgrade.actions[0].code}`; - } else if (upgrade.effects && upgrade.effects.length) { - let effects = upgrade.effects; - let attr = first(effects, effect => effect instanceof AttributeEffect || effect instanceof AttributeMultiplyEffect); - if (attr && (attr instanceof AttributeEffect || attr instanceof AttributeMultiplyEffect)) { - return `attribute-${attr.attrcode}`; - } else { - return "common-transparent"; - } - } else { - return "common-transparent"; - } - } + if (active) { + button.toggle(true); } + + builder.in(button, builder => { + if (enabled) { + builder.text(this.upgrade.code, 166, 40, { size: 16, color: "#e7ebf0", width: 210 }); + + let icon = builder.image(this.getIcon(), 40, 40, true); + if (icon.width && icon.width > 64) { + icon.setScale(64 / icon.width); + } + + range(this.upgrade.cost || 0).forEach(i => { + builder.image("character-upgrade-point", 275, 64 - i * 24, true); + }); + } else { + builder.image("character-upgrade-locked"); + } + }); + } + + /** + * Activate or deactivate the upgrade + */ + private activate(on: boolean, onchange: (selected: boolean) => void): boolean { + let oldval = this.ship.level.hasUpgrade(this.upgrade); + this.ship.activateUpgrade(this.upgrade, on); + let newval = this.ship.level.hasUpgrade(this.upgrade); + + if (newval != oldval) { + onchange(newval); + } + + return newval; + } + + /** + * Fill the tooltip for this upgrade + */ + fillTooltip(builder: TooltipBuilder): boolean { + builder.text(this.upgrade.code, 0, 0, { size: 20 }); + + let y = 42; + + if (bool(this.upgrade.effects)) { + builder.text("Permanent effects:", 0, y); + y += 30; + this.upgrade.effects.forEach(effect => { + builder.text(`• ${effect.getDescription()}`, 0, y); + y += 30; + }); + } + + if (bool(this.upgrade.actions)) { + this.upgrade.actions.forEach(action => { + action.getEffectsDescription().split('\n').forEach(line => { + builder.text(line, 0, y); + y += 30; + }) + }); + } + + if (bool(this.upgrade.description)) { + y += 10; + builder.text(this.upgrade.description, 0, y, { size: 14, color: "#999999", width: 540 }); + } + + return true; + } + + /** + * Get an icon code for an upgrade + */ + getIcon(): string { + let upgrade = this.upgrade; + + if (upgrade.actions && upgrade.actions.length) { + return `action-${upgrade.actions[0].code}`; + } else if (upgrade.effects && upgrade.effects.length) { + let effects = upgrade.effects; + let attr = first(effects, effect => effect instanceof AttributeEffect || effect instanceof AttributeMultiplyEffect); + if (attr && (attr instanceof AttributeEffect || attr instanceof AttributeMultiplyEffect)) { + return `attribute-${attr.attrcode}`; + } else { + return "common-transparent"; + } + } else { + return "common-transparent"; + } + } } diff --git a/src/ui/character/FleetCreationView.spec.ts b/src/ui/character/FleetCreationView.spec.ts index cc44e51..9b35ca9 100644 --- a/src/ui/character/FleetCreationView.spec.ts +++ b/src/ui/character/FleetCreationView.spec.ts @@ -1,36 +1,39 @@ -module TK.SpaceTac.UI.Specs { - testing("FleetCreationView", test => { - let testgame = setupSingleView(test, () => [new FleetCreationView({}), []]); +import { testing } from "../../common/Testing"; +import { UIConfirmDialog } from "../common/UIConfirmDialog"; +import { setupSingleView, testClick } from "../TestGame"; +import { FleetCreationView } from "./FleetCreationView"; - test.acase("validates the fleet creation", async check => { - let mock_router = check.patch(testgame.view, "backToRouter"); +testing("FleetCreationView", test => { + let testgame = setupSingleView(test, () => [new FleetCreationView({}), []]); - check.same(testgame.ui.session.isFleetCreated(), false, "no fleet created"); - check.same(testgame.ui.session.player.fleet.ships.length, 0, "empty session fleet"); - check.same(testgame.view.dialogs_layer.length, 0, "no dialogs"); - check.same(testgame.view.character_sheet.fleet, testgame.view.built_fleet); - check.same(testgame.view.built_fleet.ships.length, 2, "initial fleet should have two ships"); + test.acase("validates the fleet creation", async check => { + let mock_router = check.patch(testgame.view, "backToRouter"); - // close sheet - testClick(testgame.view.character_sheet.close_button); - check.same(testgame.view.dialogs_opened.length, 1, "confirmation dialog opened"); - check.same(testgame.ui.session.isFleetCreated(), false, "still no fleet created"); + check.same(testgame.ui.session.isFleetCreated(), false, "no fleet created"); + check.same(testgame.ui.session.player.fleet.ships.length, 0, "empty session fleet"); + check.same(testgame.view.dialogs_layer.length, 0, "no dialogs"); + check.same(testgame.view.character_sheet.fleet, testgame.view.built_fleet); + check.same(testgame.view.built_fleet.ships.length, 2, "initial fleet should have two ships"); - // click on no in confirmation dialog - let dialog = testgame.view.dialogs_opened[0]; - await dialog.forceResult(false); - check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'no'"); - check.same(testgame.ui.session.isFleetCreated(), false, "still no fleet created after 'no'"); - check.called(mock_router, 0); + // close sheet + testClick(testgame.view.character_sheet.close_button); + check.same(testgame.view.dialogs_opened.length, 1, "confirmation dialog opened"); + check.same(testgame.ui.session.isFleetCreated(), false, "still no fleet created"); - // close sheet, click on yes in confirmation dialog - testClick(testgame.view.character_sheet.close_button); - dialog = testgame.view.dialogs_opened[0]; - await dialog.forceResult(true); - check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'yes'"); - check.same(testgame.ui.session.isFleetCreated(), true, "fleet created"); - check.same(testgame.ui.session.player.fleet.ships.length, 2, "session fleet now has two ships"); - check.called(mock_router, 1); - }) - }) -} + // click on no in confirmation dialog + let dialog = testgame.view.dialogs_opened[0]; + await dialog.forceResult(false); + check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'no'"); + check.same(testgame.ui.session.isFleetCreated(), false, "still no fleet created after 'no'"); + check.called(mock_router, 0); + + // close sheet, click on yes in confirmation dialog + testClick(testgame.view.character_sheet.close_button); + dialog = testgame.view.dialogs_opened[0]; + await dialog.forceResult(true); + check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'yes'"); + check.same(testgame.ui.session.isFleetCreated(), true, "fleet created"); + check.same(testgame.ui.session.player.fleet.ships.length, 2, "session fleet now has two ships"); + check.called(mock_router, 1); + }) +}) diff --git a/src/ui/character/FleetCreationView.ts b/src/ui/character/FleetCreationView.ts index 77489ef..e1cace3 100644 --- a/src/ui/character/FleetCreationView.ts +++ b/src/ui/character/FleetCreationView.ts @@ -1,38 +1,43 @@ -/// +import { Fleet } from "../../core/Fleet"; +import { MissionGenerator } from "../../core/missions/MissionGenerator"; +import { ShipModel } from "../../core/models/ShipModel"; +import { Ship } from "../../core/Ship"; +import { Shop } from "../../core/Shop"; +import { BaseView } from "../BaseView"; +import { UIConfirmDialog } from "../common/UIConfirmDialog"; +import { CharacterSheet, CharacterSheetMode } from "./CharacterSheet"; -module TK.SpaceTac.UI { - /** - * View to configure the initial characters in the fleet - */ - export class FleetCreationView extends BaseView { - built_fleet!: Fleet - infinite_shop!: Shop - character_sheet!: CharacterSheet +/** + * View to configure the initial characters in the fleet + */ +export class FleetCreationView extends BaseView { + built_fleet!: Fleet + infinite_shop!: Shop + character_sheet!: CharacterSheet - create() { - super.create(); + create() { + super.create(); - let models = ShipModel.getRandomModels(2); + let models = ShipModel.getRandomModels(2); - this.built_fleet = new Fleet(); - this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[0])); - this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[1])); - this.built_fleet.credits = this.built_fleet.ships.length * 1000; + this.built_fleet = new Fleet(); + this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[0])); + this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[1])); + this.built_fleet.credits = this.built_fleet.ships.length * 1000; - this.character_sheet = new CharacterSheet(this, CharacterSheetMode.CREATION, () => this.validateFleet()); - this.character_sheet.show(this.built_fleet.ships[0], false); - this.character_sheet.moveToLayer(this.getLayer("characters")); - } + this.character_sheet = new CharacterSheet(this, CharacterSheetMode.CREATION, () => this.validateFleet()); + this.character_sheet.show(this.built_fleet.ships[0], false); + this.character_sheet.moveToLayer(this.getLayer("characters")); + } - /** - * Validate the configured fleet and move on - */ - async validateFleet() { - let confirmed = await UIConfirmDialog.ask(this, "Do you confirm these initial fleet settings ?"); - if (confirmed) { - this.session.setCampaignFleet(this.built_fleet, this.session.hasUniverse()); - this.backToRouter(); - } - } + /** + * Validate the configured fleet and move on + */ + async validateFleet() { + let confirmed = await UIConfirmDialog.ask(this, "Do you confirm these initial fleet settings ?"); + if (confirmed) { + this.session.setCampaignFleet(this.built_fleet, this.session.hasUniverse()); + this.backToRouter(); } + } } diff --git a/src/ui/common/Animations.spec.ts b/src/ui/common/Animations.spec.ts index 832590d..13ff75f 100644 --- a/src/ui/common/Animations.spec.ts +++ b/src/ui/common/Animations.spec.ts @@ -1,84 +1,86 @@ -module TK.SpaceTac.UI.Specs { - testing("Animations", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { UIBuilder } from "./UIBuilder"; - test.case("shows and hides objects", check => { - let obj = { visible: false, alpha: 0.5 }; +testing("Animations", test => { + let testgame = setupEmptyView(test); - check.equals(testgame.view.animations.simulate(obj, 'alpha'), []); + test.case("shows and hides objects", check => { + let obj = { visible: false, alpha: 0.5 }; - testgame.view.animations.show(obj); + check.equals(testgame.view.animations.simulate(obj, 'alpha'), []); - check.equals(obj.visible, true); - check.equals(obj.alpha, 0); - check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0, 0.25, 0.5, 0.75, 1]); + testgame.view.animations.show(obj); - obj.alpha = 1; - testgame.view.animations.hide(obj); + check.equals(obj.visible, true); + check.equals(obj.alpha, 0); + check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0, 0.25, 0.5, 0.75, 1]); - check.equals(obj.visible, true); - check.equals(obj.alpha, 1); - check.equals(testgame.view.animations.simulate(obj, 'alpha'), [1, 0.75, 0.5, 0.25, 0]); + obj.alpha = 1; + testgame.view.animations.hide(obj); - obj.alpha = 0.2; - testgame.view.animations.setVisible(obj, true, 1000, 0.6); + check.equals(obj.visible, true); + check.equals(obj.alpha, 1); + check.equals(testgame.view.animations.simulate(obj, 'alpha'), [1, 0.75, 0.5, 0.25, 0]); - check.equals(obj.visible, true); - check.equals(obj.alpha, 0.2); - check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0.2, 0.3, 0.4, 0.5, 0.6]); + obj.alpha = 0.2; + testgame.view.animations.setVisible(obj, true, 1000, 0.6); - obj.alpha = 0.6; - testgame.view.animations.setVisible(obj, false, 1000, 0.6, 0.2); + check.equals(obj.visible, true); + check.equals(obj.alpha, 0.2); + check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0.2, 0.3, 0.4, 0.5, 0.6]); - check.equals(obj.visible, true); - check.equals(obj.alpha, 0.6); - check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0.6, 0.5, 0.4, 0.3, 0.2]); - }); + obj.alpha = 0.6; + testgame.view.animations.setVisible(obj, false, 1000, 0.6, 0.2); - test.case("blocks input while object is hidden", check => { - let changeStateFrame = check.mockfunc("changeStateFrame"); - let obj = { visible: true, alpha: 1, input: { enabled: true }, changeStateFrame: changeStateFrame.func, freezeFrames: false }; + check.equals(obj.visible, true); + check.equals(obj.alpha, 0.6); + check.equals(testgame.view.animations.simulate(obj, 'alpha'), [0.6, 0.5, 0.4, 0.3, 0.2]); + }); - testgame.view.animations.setVisible(obj, false, 0); - check.equals(obj.visible, false); - check.equals(obj.alpha, 0); - check.equals(obj.input.enabled, false); - check.called(changeStateFrame, [["Out"]]) - check.equals(obj.freezeFrames, true); + test.case("blocks input while object is hidden", check => { + let changeStateFrame = check.mockfunc("changeStateFrame"); + let obj = { visible: true, alpha: 1, input: { enabled: true }, changeStateFrame: changeStateFrame.func, freezeFrames: false }; - testgame.view.animations.setVisible(obj, true, 0); - check.equals(obj.visible, true); - check.equals(obj.alpha, 1); - check.equals(obj.input.enabled, true); - check.called(changeStateFrame, 0); - check.equals(obj.freezeFrames, false); - }); + testgame.view.animations.setVisible(obj, false, 0); + check.equals(obj.visible, false); + check.equals(obj.alpha, 0); + check.equals(obj.input.enabled, false); + check.called(changeStateFrame, [["Out"]]) + check.equals(obj.freezeFrames, true); - test.case("animates rotation", check => { - let obj = new UIBuilder(testgame.view).image("test"); - obj.setRotation(-Math.PI * 2.5); - testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear"); - let points = testgame.view.animations.simulate(obj, "rotation", 4); - check.nears(points[0], -Math.PI * 0.5); - check.nears(points[1], -Math.PI * 0.25); - check.nears(points[2], 0); - check.nears(points[3], Math.PI * 0.25); - }); + testgame.view.animations.setVisible(obj, true, 0); + check.equals(obj.visible, true); + check.equals(obj.alpha, 1); + check.equals(obj.input.enabled, true); + check.called(changeStateFrame, 0); + check.equals(obj.freezeFrames, false); + }); - test.case("stops previous animations before starting a new one", check => { - let obj = { x: 0, y: 0 }; - testgame.view.animations.addAnimation(obj, { x: 1 }, 1000); - testgame.clockForward(1); - testgame.clockForward(1); - check.equals(testgame.view.tweens.getAllTweens().length, 1); - testgame.view.animations.addAnimation(obj, { y: 1 }, 1000); - testgame.clockForward(1); - testgame.clockForward(1); - check.equals(testgame.view.tweens.getAllTweens().length, 2); - testgame.view.animations.addAnimation(obj, { x: 2 }, 1000); - testgame.clockForward(1); - testgame.clockForward(1); - check.equals(testgame.view.tweens.getAllTweens().length, 2); - }); - }); -} + test.case("animates rotation", check => { + let obj = new UIBuilder(testgame.view).image("test"); + obj.setRotation(-Math.PI * 2.5); + testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear"); + let points = testgame.view.animations.simulate(obj, "rotation", 4); + check.nears(points[0], -Math.PI * 0.5); + check.nears(points[1], -Math.PI * 0.25); + check.nears(points[2], 0); + check.nears(points[3], Math.PI * 0.25); + }); + + test.case("stops previous animations before starting a new one", check => { + let obj = { x: 0, y: 0 }; + testgame.view.animations.addAnimation(obj, { x: 1 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 1); + testgame.view.animations.addAnimation(obj, { y: 1 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 2); + testgame.view.animations.addAnimation(obj, { x: 2 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 2); + }); +}); diff --git a/src/ui/common/Animations.ts b/src/ui/common/Animations.ts index 6b74a04..3a966f2 100644 --- a/src/ui/common/Animations.ts +++ b/src/ui/common/Animations.ts @@ -1,299 +1,304 @@ -module TK.SpaceTac.UI { - interface PhaserGraphics { - x: number - y: number - rotation: number - }; +import { Timer } from "../../common/Timer"; +import { Toggle } from "../../common/Toggle"; +import { any, bool, coalesce, contains, copyfields, first, keys, merge, nop, range } from "../../common/Tools"; +import { angularDifference, arenaDistance } from "../../core/ArenaLocation"; +import { Target } from "../../core/Target"; +import { UITools } from "./UITools"; - /** - * Interface of an object that may be shown/hidden, with opacity transition. - */ - interface IAnimationFadeable { - alpha: number - visible: boolean - input?: { enabled: boolean } - changeStateFrame?: Function - freezeFrames?: boolean - } +interface PhaserGraphics { + x: number + y: number + rotation: number +}; - /** - * Configuration object for blink animations - */ - interface AnimationBlinkOptions { - alpha_on?: number - alpha_off?: number - times?: number - speed?: number - } - - /** - * Manager of all animations. - * - * This is a wrapper around phaser's tweens. - */ - export class Animations { - private immediate = false - - constructor(private tweens: Phaser.Tweens.TweenManager) { - } - - /** - * Set all future animations to be immediate (and synchronous) - * - * This is mostly useful in tests - */ - setImmediate(immediate = true): void { - this.immediate = immediate; - } - - /** - * Kill previous tweens currently running on an object's properties - */ - killPrevious(obj: T, properties: Extract[]): void { - this.tweens.getTweensOf(obj).forEach(tween => { - if (tween.data && any(tween.data, data => bool(data.key) && contains(properties, data.key))) { - tween.stop(); - } - }); - } - - /** - * Simulate the tween currently applied to an object's property - * - * This may be heavy work and should only be done in testing code. - */ - simulate(obj: any, property: string, points = 5): number[] { - this.tweens.preUpdate(); - let tween = first(this.tweens.getTweensOf(obj), tween => tween.isPlaying()); - if (tween) { - let tween_obj = tween; - tween_obj.update(0, 0); - return range(points).map(i => { - tween_obj.seek(i / (points - 1)); - return obj[property]; - }); - } else { - return []; - } - } - - /** - * Display an object, with opacity transition - */ - show(obj: IAnimationFadeable, duration = 1000, alpha = 1): void { - this.killPrevious(obj, ['alpha']); - - if (!obj.visible) { - obj.alpha = 0; - obj.visible = true; - } - - let onComplete: Function | undefined; - if (obj.input) { - let input = obj.input; - onComplete = () => { - input.enabled = true - obj.freezeFrames = false; - }; - } - - if (duration && !this.immediate) { - this.tweens.add({ - targets: obj, - alpha: alpha, - duration: duration, - onComplete: onComplete - }) - } else { - obj.alpha = alpha; - if (onComplete) { - onComplete(); - } - } - } - - /** - * Hide an object, with opacity transition - */ - hide(obj: IAnimationFadeable, duration = 1000, alpha = 0): void { - this.killPrevious(obj, ['alpha']); - - if (obj.changeStateFrame) { - obj.changeStateFrame("Out"); - obj.freezeFrames = true; - } - - if (obj.input) { - obj.input.enabled = false; - } - - let onComplete = () => obj.visible = alpha > 0; - - if (duration && !this.immediate) { - this.tweens.add({ - targets: obj, - alpha: alpha, - duration: duration, - onComplete: onComplete - }); - } else { - obj.alpha = alpha; - onComplete(); - } - } - - /** - * Set an object visibility, with opacity transition - */ - setVisible(obj: IAnimationFadeable, visible: boolean, duration = 1000, alphaon = 1, alphaoff = 0): void { - if (visible) { - this.show(obj, duration, alphaon); - } else { - this.hide(obj, duration, alphaoff); - } - } - - /** - * Get a toggle on visibility - */ - newVisibilityToggle(obj: IAnimationFadeable, duration = 1000, initial = true): Toggle { - let result = new Toggle(() => this.setVisible(obj, true, duration), () => this.setVisible(obj, false, duration)); - this.setVisible(obj, initial, 0); - return result; - } - - /** - * Add an asynchronous animation to an object. - */ - addAnimation(obj: T, properties: Partial, duration: number, ease = "Linear", delay = 0, loop = 1, yoyo = false): Promise { - this.killPrevious(obj, keys(properties)); - - if (!duration) { - copyfields(properties, obj); - return Promise.resolve(); - } - - return new Promise(resolve => { - this.tweens.add(merge({ - targets: obj, - ease: ease, - duration: duration, - delay: delay, - loop: loop - 1, - onComplete: resolve, - yoyo: yoyo - }, properties)); - - // By security, if the tween is destroyed before completion, we resolve the promise using the timer - Timer.global.schedule(delay + duration, resolve); - }); - } - - /** - * Catch the player eye with a blink effect - */ - async blink(obj: { alpha: number }, config: AnimationBlinkOptions = {}): Promise { - let speed = coalesce(config.speed, 1); - let alpha_on = coalesce(config.alpha_on, 1); - - if (!speed) { - obj.alpha = alpha_on; - } - - let alpha_off = coalesce(config.alpha_off, 0.3); - let times = coalesce(config.times, 3); - - if (obj.alpha != alpha_on) { - await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); - } - for (let i = 0; i < times; i++) { - await this.addAnimation(obj, { alpha: alpha_off }, 150 / speed); - await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); - } - } - - /** - * Interpolate a rotation value - * - * This will take into account the 2*pi modulo - * - * Returns the duration - */ - rotationTween(obj: Phaser.GameObjects.Components.Transform, dest: number, speed = 1, easing = "Cubic.easeInOut"): Promise { - // Immediately change the object's current rotation to be in range (-pi,pi) - let value = UITools.normalizeAngle(obj.rotation); - obj.setRotation(value); - - // Compute destination angle - dest = UITools.normalizeAngle(dest); - if (value - dest > Math.PI) { - dest += 2 * Math.PI; - } else if (value - dest < -Math.PI) { - dest -= 2 * Math.PI; - } - let distance = Math.abs(UITools.normalizeAngle(dest - value)) / Math.PI; - let duration = distance * 2000 / speed; - - // Tween - return this.addAnimation(obj, { rotation: dest }, duration, easing); - } - - /** - * Move an object linearly to another position - * - * Returns the animation duration. - */ - moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1, ease = true): Promise { - let duration = arenaDistance(obj, { x: x, y: y }) * 2 / speed; - - return Promise.all([ - this.rotationTween(rotated_obj, angle, speed, ease ? "Cubic.easeInOut" : "Linear"), - this.addAnimation(obj, { x: x, y: y }, duration, ease ? "Quad.easeInOut" : "Linear"), - ]).then(nop); - } - - /** - * Make an object move toward a location in space, with a ship-like animation. - * - * Returns the animation duration. - */ - moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1): Promise { - this.killPrevious(obj, ["x", "y"]); - - if (x == obj.x && y == obj.y) { - let distance = Math.abs(angularDifference(rotated_obj.rotation, angle)); - return this.rotationTween(rotated_obj, angle, speed); - } else { - this.killPrevious(rotated_obj, ["rotation"]); - let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); - let duration = Math.sqrt(distance / 1000) * 3000 / speed; - let curve_force = distance * 0.4; - let prevx = obj.x; - let prevy = obj.y; - let xpts = [obj.x, obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x]; - let ypts = [obj.y, obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y]; - let fobj = { t: 0 }; - return new Promise(resolve => { - this.tweens.add({ - targets: [fobj], - t: 1, - duration: duration, - ease: "Sine.easeInOut", - onComplete: resolve, - onUpdate: () => { - obj.setPosition( - Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]), - Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]), - ) - if (prevx != obj.x || prevy != obj.y) { - rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx)); - } - prevx = obj.x; - prevy = obj.y; - } - }) - }); - } - } - } +/** + * Interface of an object that may be shown/hidden, with opacity transition. + */ +interface IAnimationFadeable { + alpha: number + visible: boolean + input?: { enabled: boolean } + changeStateFrame?: Function + freezeFrames?: boolean +} + +/** + * Configuration object for blink animations + */ +interface AnimationBlinkOptions { + alpha_on?: number + alpha_off?: number + times?: number + speed?: number +} + +/** + * Manager of all animations. + * + * This is a wrapper around phaser's tweens. + */ +export class Animations { + private immediate = false + + constructor(private tweens: Phaser.Tweens.TweenManager) { + } + + /** + * Set all future animations to be immediate (and synchronous) + * + * This is mostly useful in tests + */ + setImmediate(immediate = true): void { + this.immediate = immediate; + } + + /** + * Kill previous tweens currently running on an object's properties + */ + killPrevious(obj: T, properties: Extract[]): void { + this.tweens.getTweensOf(obj).forEach(tween => { + if (tween.data && any(tween.data, data => bool(data.key) && contains(properties, data.key))) { + tween.stop(); + } + }); + } + + /** + * Simulate the tween currently applied to an object's property + * + * This may be heavy work and should only be done in testing code. + */ + simulate(obj: any, property: string, points = 5): number[] { + this.tweens.preUpdate(); + let tween = first(this.tweens.getTweensOf(obj), tween => tween.isPlaying()); + if (tween) { + let tween_obj = tween; + tween_obj.update(0, 0); + return range(points).map(i => { + tween_obj.seek(i / (points - 1)); + return obj[property]; + }); + } else { + return []; + } + } + + /** + * Display an object, with opacity transition + */ + show(obj: IAnimationFadeable, duration = 1000, alpha = 1): void { + this.killPrevious(obj, ['alpha']); + + if (!obj.visible) { + obj.alpha = 0; + obj.visible = true; + } + + let onComplete: any; + if (obj.input) { + let input = obj.input; + onComplete = () => { + input.enabled = true + obj.freezeFrames = false; + }; + } + + if (duration && !this.immediate) { + this.tweens.add({ + targets: obj, + alpha: alpha, + duration: duration, + onComplete: onComplete + }) + } else { + obj.alpha = alpha; + if (onComplete) { + onComplete(); + } + } + } + + /** + * Hide an object, with opacity transition + */ + hide(obj: IAnimationFadeable, duration = 1000, alpha = 0): void { + this.killPrevious(obj, ['alpha']); + + if (obj.changeStateFrame) { + obj.changeStateFrame("Out"); + obj.freezeFrames = true; + } + + if (obj.input) { + obj.input.enabled = false; + } + + let onComplete = () => obj.visible = alpha > 0; + + if (duration && !this.immediate) { + this.tweens.add({ + targets: obj, + alpha: alpha, + duration: duration, + onComplete: onComplete + }); + } else { + obj.alpha = alpha; + onComplete(); + } + } + + /** + * Set an object visibility, with opacity transition + */ + setVisible(obj: IAnimationFadeable, visible: boolean, duration = 1000, alphaon = 1, alphaoff = 0): void { + if (visible) { + this.show(obj, duration, alphaon); + } else { + this.hide(obj, duration, alphaoff); + } + } + + /** + * Get a toggle on visibility + */ + newVisibilityToggle(obj: IAnimationFadeable, duration = 1000, initial = true): Toggle { + let result = new Toggle(() => this.setVisible(obj, true, duration), () => this.setVisible(obj, false, duration)); + this.setVisible(obj, initial, 0); + return result; + } + + /** + * Add an asynchronous animation to an object. + */ + addAnimation(obj: T, properties: Partial, duration: number, ease = "Linear", delay = 0, loop = 1, yoyo = false): Promise { + this.killPrevious(obj, keys(properties)); + + if (!duration) { + copyfields(properties, obj); + return Promise.resolve(); + } + + return new Promise(resolve => { + this.tweens.add(merge({ + targets: obj, + ease: ease, + duration: duration, + delay: delay, + loop: loop - 1, + onComplete: resolve, + yoyo: yoyo + }, properties)); + + // By security, if the tween is destroyed before completion, we resolve the promise using the timer + Timer.global.schedule(delay + duration, resolve); + }); + } + + /** + * Catch the player eye with a blink effect + */ + async blink(obj: { alpha: number }, config: AnimationBlinkOptions = {}): Promise { + let speed = coalesce(config.speed, 1); + let alpha_on = coalesce(config.alpha_on, 1); + + if (!speed) { + obj.alpha = alpha_on; + } + + let alpha_off = coalesce(config.alpha_off, 0.3); + let times = coalesce(config.times, 3); + + if (obj.alpha != alpha_on) { + await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); + } + for (let i = 0; i < times; i++) { + await this.addAnimation(obj, { alpha: alpha_off }, 150 / speed); + await this.addAnimation(obj, { alpha: alpha_on }, 150 / speed); + } + } + + /** + * Interpolate a rotation value + * + * This will take into account the 2*pi modulo + * + * Returns the duration + */ + rotationTween(obj: Phaser.GameObjects.Components.Transform, dest: number, speed = 1, easing = "Cubic.easeInOut"): Promise { + // Immediately change the object's current rotation to be in range (-pi,pi) + let value = UITools.normalizeAngle(obj.rotation); + obj.setRotation(value); + + // Compute destination angle + dest = UITools.normalizeAngle(dest); + if (value - dest > Math.PI) { + dest += 2 * Math.PI; + } else if (value - dest < -Math.PI) { + dest -= 2 * Math.PI; + } + let distance = Math.abs(UITools.normalizeAngle(dest - value)) / Math.PI; + let duration = distance * 2000 / speed; + + // Tween + return this.addAnimation(obj, { rotation: dest }, duration, easing); + } + + /** + * Move an object linearly to another position + * + * Returns the animation duration. + */ + moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1, ease = true): Promise { + let duration = arenaDistance(obj, { x: x, y: y }) * 2 / speed; + + return Promise.all([ + this.rotationTween(rotated_obj, angle, speed, ease ? "Cubic.easeInOut" : "Linear"), + this.addAnimation(obj, { x: x, y: y }, duration, ease ? "Quad.easeInOut" : "Linear"), + ]).then(nop); + } + + /** + * Make an object move toward a location in space, with a ship-like animation. + * + * Returns the animation duration. + */ + moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, speed = 1): Promise { + this.killPrevious(obj, ["x", "y"]); + + if (x == obj.x && y == obj.y) { + let distance = Math.abs(angularDifference(rotated_obj.rotation, angle)); + return this.rotationTween(rotated_obj, angle, speed); + } else { + this.killPrevious(rotated_obj, ["rotation"]); + let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); + let duration = Math.sqrt(distance / 1000) * 3000 / speed; + let curve_force = distance * 0.4; + let prevx = obj.x; + let prevy = obj.y; + let xpts = [obj.x, obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x]; + let ypts = [obj.y, obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y]; + let fobj = { t: 0 }; + return new Promise(resolve => { + this.tweens.add({ + targets: [fobj], + t: 1, + duration: duration, + ease: "Sine.easeInOut", + onComplete: () => resolve(), + onUpdate: () => { + obj.setPosition( + Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]), + Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]), + ) + if (prevx != obj.x || prevy != obj.y) { + rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx)); + } + prevx = obj.x; + prevy = obj.y; + } + }) + }); + } + } } diff --git a/src/ui/common/Audio.ts b/src/ui/common/Audio.ts deleted file mode 100644 index ef40d4b..0000000 --- a/src/ui/common/Audio.ts +++ /dev/null @@ -1,130 +0,0 @@ -module TK.SpaceTac.UI { - class AudioSettings { - main_volume = 1 - music_volume = 1 - } - - /** - * Utility functions to play sounds and musics - */ - export class Audio { - private static SETTINGS = new AudioSettings() - private music: Phaser.Sound.BaseSound | undefined - private music_playing_volume = 1 - - constructor(private game: MainUI) { - } - - /** - * Check if the sound system is active, and return a manager to operate with it - */ - private getManager(): Phaser.Sound.BaseSoundManager | null { - return this.game.sound; - } - - /** - * Check if an audio key is present in cache - */ - hasCache(key: string): boolean { - return this.game.cache.audio.has(key); - } - - /** - * Play a single sound effect (fire-and-forget) - */ - playOnce(key: string, speed = 1): void { - if (speed != 1) { - // TODO - return; - } - - let manager = this.getManager(); - if (manager) { - if (this.hasCache(key)) { - manager.play(key); - } else { - console.warn("Missing sound", key); - } - } - } - - /** - * Start a background music in repeat - */ - startMusic(key: string, volume = 1): void { - let manager = this.getManager(); - if (manager) { - this.stopMusic(); - - if (!this.music) { - key = "music-" + key; - if (this.hasCache(key)) { - this.music_playing_volume = volume; - this.music = manager.add(key, { - volume: volume * Audio.SETTINGS.music_volume, - loop: true - }); - this.music.play(); - } else { - console.warn("Missing music", key); - } - } - } - } - - /** - * Stop currently playing background music - */ - stopMusic(): void { - let music = this.music; - if (music) { - music.stop(); - music.destroy(); - this.music = undefined; - } - } - - /** - * Get the main volume (0-1) - */ - getMainVolume(): number { - return Audio.SETTINGS.main_volume; - } - - /** - * Set the main volume (0-1) - */ - setMainVolume(value: number) { - Audio.SETTINGS.main_volume = clamp(value, 0, 1); - - let manager = this.getManager(); - if (manager) { - manager.volume = Audio.SETTINGS.main_volume; - } - } - - /** - * Get the music volume (0-1) - */ - getMusicVolume(): number { - return Audio.SETTINGS.music_volume; - } - - /** - * Set the music volume (0-1) - */ - setMusicVolume(value: number) { - Audio.SETTINGS.music_volume = clamp(value, 0, 1); - - let music = this.music; - if (music) { - // TODO Set music volume - if (Audio.SETTINGS.music_volume) { - music.resume(); - } else { - music.pause(); - } - } - } - } -} diff --git a/src/ui/common/AudioManager.ts b/src/ui/common/AudioManager.ts new file mode 100644 index 0000000..14b17dc --- /dev/null +++ b/src/ui/common/AudioManager.ts @@ -0,0 +1,131 @@ +import { clamp } from "../../common/Tools" +import { MainUI } from "../../MainUI" + +class AudioSettings { + main_volume = 1 + music_volume = 1 +} + +/** + * Utility functions to play sounds and musics + */ +export class AudioManager { + private static SETTINGS = new AudioSettings() + private music: Phaser.Sound.BaseSound | undefined + private music_playing_volume = 1 + + constructor(private game: MainUI) { + } + + /** + * Check if the sound system is active, and return a manager to operate with it + */ + private getManager(): Phaser.Sound.BaseSoundManager | null { + return this.game.sound; + } + + /** + * Check if an audio key is present in cache + */ + hasCache(key: string): boolean { + return this.game.cache.audio.has(key); + } + + /** + * Play a single sound effect (fire-and-forget) + */ + playOnce(key: string, speed = 1): void { + if (speed != 1) { + // TODO + return; + } + + let manager = this.getManager(); + if (manager) { + if (this.hasCache(key)) { + manager.play(key); + } else { + console.warn("Missing sound", key); + } + } + } + + /** + * Start a background music in repeat + */ + startMusic(key: string, volume = 1): void { + let manager = this.getManager(); + if (manager) { + this.stopMusic(); + + if (!this.music) { + key = "music-" + key; + if (this.hasCache(key)) { + this.music_playing_volume = volume; + this.music = manager.add(key, { + volume: volume * AudioManager.SETTINGS.music_volume, + loop: true + }); + this.music.play(); + } else { + console.warn("Missing music", key); + } + } + } + } + + /** + * Stop currently playing background music + */ + stopMusic(): void { + let music = this.music; + if (music) { + music.stop(); + music.destroy(); + this.music = undefined; + } + } + + /** + * Get the main volume (0-1) + */ + getMainVolume(): number { + return AudioManager.SETTINGS.main_volume; + } + + /** + * Set the main volume (0-1) + */ + setMainVolume(value: number) { + AudioManager.SETTINGS.main_volume = clamp(value, 0, 1); + + let manager = this.getManager(); + if (manager) { + manager.volume = AudioManager.SETTINGS.main_volume; + } + } + + /** + * Get the music volume (0-1) + */ + getMusicVolume(): number { + return AudioManager.SETTINGS.music_volume; + } + + /** + * Set the music volume (0-1) + */ + setMusicVolume(value: number) { + AudioManager.SETTINGS.music_volume = clamp(value, 0, 1); + + let music = this.music; + if (music) { + // TODO Set music volume + if (AudioManager.SETTINGS.music_volume) { + music.resume(); + } else { + music.pause(); + } + } + } +} diff --git a/src/ui/common/InputManager.spec.ts b/src/ui/common/InputManager.spec.ts index daca9bd..d041ab7 100644 --- a/src/ui/common/InputManager.spec.ts +++ b/src/ui/common/InputManager.spec.ts @@ -1,113 +1,115 @@ -module TK.SpaceTac.UI.Specs { - testing("InputManager", test => { - let testgame = setupEmptyView(test); +import { Mock, testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { UIImage } from "./UIImage"; - test.case("handles hover and click on desktops and mobile targets", check => { - let inputs = testgame.view.inputs; +testing("InputManager", test => { + let testgame = setupEmptyView(test); - let pointer = new Phaser.Input.Pointer(testgame.view.input.manager, 0); - pointer.buttons = 1; - function newButton(): [UIImage, { enter: Mock, leave: Mock, click: Mock }] { - let button = new UIImage(testgame.view, 0, 0, "fake"); - let mocks = { - enter: check.mockfunc("enter"), - leave: check.mockfunc("leave"), - click: check.mockfunc("click"), - }; - inputs.setHoverClick(button, mocks.enter.func, mocks.leave.func, mocks.click.func, 50, 100); - (inputs).hovered = null; - return [button, mocks]; - } - let enter = (button: UIImage) => button.emit("pointerover", pointer); - let leave = (button: UIImage) => button.emit("pointerout", pointer); - let press = (button: UIImage) => button.emit("pointerdown", pointer); - let release = (button: UIImage) => button.emit("pointerup", pointer); - let destroy = (button: UIImage) => button.emit("destroy"); + test.case("handles hover and click on desktops and mobile targets", check => { + let inputs = testgame.view.inputs; - let [button, mocks] = newButton(); - check.in("Simple click on desktop", check => { - enter(button); - press(button); - release(button); - check.called(mocks.enter, 0); - check.called(mocks.leave, 0); - check.called(mocks.click, 1); - }); + let pointer = new Phaser.Input.Pointer(testgame.view.input.manager, 0); + pointer.buttons = 1; + function newButton(): [UIImage, { enter: Mock, leave: Mock, click: Mock }] { + let button = new UIImage(testgame.view, 0, 0, "fake"); + let mocks = { + enter: check.mockfunc("enter"), + leave: check.mockfunc("leave"), + click: check.mockfunc("click"), + }; + inputs.setHoverClick(button, mocks.enter.func, mocks.leave.func, mocks.click.func, 50, 100); + (inputs).hovered = null; + return [button, mocks]; + } + let enter = (button: UIImage) => button.emit("pointerover", pointer); + let leave = (button: UIImage) => button.emit("pointerout", pointer); + let press = (button: UIImage) => button.emit("pointerdown", pointer); + let release = (button: UIImage) => button.emit("pointerup", pointer); + let destroy = (button: UIImage) => button.emit("destroy"); - [button, mocks] = newButton(); - check.in("Simple click on mobile", check => { - press(button); - release(button); - check.called(mocks.enter, 1); - check.called(mocks.leave, 1); - check.called(mocks.click, 1); - }); - - [button, mocks] = newButton(); - check.in("Leaves on destroy", check => { - press(button); - testgame.clockForward(150); - check.called(mocks.enter, 1); - check.called(mocks.leave, 0); - check.called(mocks.click, 0); - destroy(button); - check.called(mocks.enter, 0); - check.called(mocks.leave, 1); - check.called(mocks.click, 0); - press(button); - release(button); - check.called(mocks.enter, 0); - check.called(mocks.leave, 0); - check.called(mocks.click, 0); - }); - - check.in("Force-leave when hovering another button without clean leaving a first one", check => { - let [button1, funcs1] = newButton(); - let [button2, funcs2] = newButton(); - enter(button1); - testgame.clockForward(150); - check.called(funcs1.enter, 1); - check.called(funcs1.leave, 0); - check.called(funcs1.click, 0); - enter(button2); - check.called(funcs1.enter, 0); - check.called(funcs1.leave, 1); - check.called(funcs1.click, 0); - check.called(funcs2.enter, 0); - check.called(funcs2.leave, 0); - check.called(funcs2.click, 0); - testgame.clockForward(150); - check.called(funcs1.enter, 0); - check.called(funcs1.leave, 0); - check.called(funcs1.click, 0); - check.called(funcs2.enter, 1); - check.called(funcs2.leave, 0); - check.called(funcs2.click, 0); - }); - - [button, mocks] = newButton(); - check.in("Hold to hover on mobile", check => { - button.emit("pointerdown", pointer); - testgame.clockForward(150); - check.called(mocks.enter, 1); - check.called(mocks.leave, 0); - check.called(mocks.click, 0); - button.emit("pointerup", pointer); - check.called(mocks.enter, 0); - check.called(mocks.leave, 1); - check.called(mocks.click, 0); - }); - - [button, mocks] = newButton(); - pointer.buttons = 2; - check.in("No right button", check => { - enter(button); - press(button); - release(button); - check.called(mocks.enter, 0); - check.called(mocks.leave, 0); - check.called(mocks.click, 0); - }); - }); + let [button, mocks] = newButton(); + check.in("Simple click on desktop", check => { + enter(button); + press(button); + release(button); + check.called(mocks.enter, 0); + check.called(mocks.leave, 0); + check.called(mocks.click, 1); }); -} + + [button, mocks] = newButton(); + check.in("Simple click on mobile", check => { + press(button); + release(button); + check.called(mocks.enter, 1); + check.called(mocks.leave, 1); + check.called(mocks.click, 1); + }); + + [button, mocks] = newButton(); + check.in("Leaves on destroy", check => { + press(button); + testgame.clockForward(150); + check.called(mocks.enter, 1); + check.called(mocks.leave, 0); + check.called(mocks.click, 0); + destroy(button); + check.called(mocks.enter, 0); + check.called(mocks.leave, 1); + check.called(mocks.click, 0); + press(button); + release(button); + check.called(mocks.enter, 0); + check.called(mocks.leave, 0); + check.called(mocks.click, 0); + }); + + check.in("Force-leave when hovering another button without clean leaving a first one", check => { + let [button1, funcs1] = newButton(); + let [button2, funcs2] = newButton(); + enter(button1); + testgame.clockForward(150); + check.called(funcs1.enter, 1); + check.called(funcs1.leave, 0); + check.called(funcs1.click, 0); + enter(button2); + check.called(funcs1.enter, 0); + check.called(funcs1.leave, 1); + check.called(funcs1.click, 0); + check.called(funcs2.enter, 0); + check.called(funcs2.leave, 0); + check.called(funcs2.click, 0); + testgame.clockForward(150); + check.called(funcs1.enter, 0); + check.called(funcs1.leave, 0); + check.called(funcs1.click, 0); + check.called(funcs2.enter, 1); + check.called(funcs2.leave, 0); + check.called(funcs2.click, 0); + }); + + [button, mocks] = newButton(); + check.in("Hold to hover on mobile", check => { + button.emit("pointerdown", pointer); + testgame.clockForward(150); + check.called(mocks.enter, 1); + check.called(mocks.leave, 0); + check.called(mocks.click, 0); + button.emit("pointerup", pointer); + check.called(mocks.enter, 0); + check.called(mocks.leave, 1); + check.called(mocks.click, 0); + }); + + [button, mocks] = newButton(); + pointer.buttons = 2; + check.in("No right button", check => { + enter(button); + press(button); + release(button); + check.called(mocks.enter, 0); + check.called(mocks.leave, 0); + check.called(mocks.click, 0); + }); + }); +}); diff --git a/src/ui/common/InputManager.ts b/src/ui/common/InputManager.ts index d473a0a..90e1181 100644 --- a/src/ui/common/InputManager.ts +++ b/src/ui/common/InputManager.ts @@ -1,274 +1,281 @@ -module TK.SpaceTac.UI { - export type KeyPressedCallback = (key: string) => void +import { Timer } from "../../common/Timer" +import { contains, nop } from "../../common/Tools" +import { MainUI } from "../../MainUI" +import { BaseView } from "../BaseView" +import { UIButton } from "./UIButton" +import { UIContainer } from "./UIContainer" +import { UIImage } from "./UIImage" +import { UITools } from "./UITools" - /** - * Manager for keyboard/mouse/touch events. - */ - export class InputManager { - private debug = false - private view: BaseView - private game: MainUI +export type KeyPressedCallback = (key: string) => void - private cheats_allowed: boolean - private cheat: boolean +/** + * Manager for keyboard/mouse/touch events. + */ +export class InputManager { + private debug = false + private view: BaseView + private game: MainUI - private hovered: UIButton | UIContainer | UIImage | null = null + private cheats_allowed: boolean + private cheat: boolean - private binds: { [key: string]: KeyPressedCallback } = {} + private hovered: UIButton | UIContainer | UIImage | null = null - private keyboard_grabber: any = null - private keyboard_callback: KeyPressedCallback | null = null + private binds: { [key: string]: KeyPressedCallback } = {} - constructor(view: BaseView) { - this.view = view; - this.game = view.gameui; - this.cheats_allowed = true; - this.cheat = false; + private keyboard_grabber: any = null + private keyboard_callback: KeyPressedCallback | null = null - // Default mappings - this.bind("s", "Quick save", () => { - this.game.saveGame(); - }); - this.bind("l", "Quick load", () => { - this.game.loadGame(); - this.view.backToRouter(); - }); - this.bind("m", "Toggle sound", () => { - this.game.options.setNumberValue("mainvolume", this.game.options.getNumberValue("mainvolume") > 0 ? 0 : 1); - }); - this.bind("f", "Toggle fullscreen", () => { - this.game.options.setBooleanValue("fullscreen", !this.game.options.getBooleanValue("fullscreen")); - }); - this.bind("+", "", () => { - if (this.cheats_allowed) { - this.cheat = !this.cheat; - this.game.displayMessage(this.cheat ? "Cheats enabled" : "Cheats disabled"); - } - }); + constructor(view: BaseView) { + this.view = view; + this.game = view.gameui; + this.cheats_allowed = true; + this.cheat = false; - if (!this.game.isTesting) { - this.view.input.keyboard.on("keyup", (event: KeyboardEvent) => { - if (this.debug) { - console.log(event); - } + // Default mappings + this.bind("s", "Quick save", () => { + this.game.saveGame(); + }); + this.bind("l", "Quick load", () => { + this.game.loadGame(); + this.view.backToRouter(); + }); + this.bind("m", "Toggle sound", () => { + this.game.options.setNumberValue("mainvolume", this.game.options.getNumberValue("mainvolume") > 0 ? 0 : 1); + }); + this.bind("f", "Toggle fullscreen", () => { + this.game.options.setBooleanValue("fullscreen", !this.game.options.getBooleanValue("fullscreen")); + }); + this.bind("+", "", () => { + if (this.cheats_allowed) { + this.cheat = !this.cheat; + this.game.displayMessage(this.cheat ? "Cheats enabled" : "Cheats disabled"); + } + }); - this.forceLeaveHovered(); - - if (!contains(["Control", "Shift", "Alt", "Meta"], event.key)) { - this.keyPress(event.key); - if (event.code != event.key) { - this.keyPress(event.code); - } - } - }); - } + if (!this.game.isTesting) { + this.view.input.keyboard.on("keyup", (event: KeyboardEvent) => { + if (this.debug) { + console.log(event); } - /** - * Remove the bindings - */ - destroy(): void { - this.view.input.keyboard.removeAllListeners("keyup"); - } - - /** - * Bind a key to a specific action. - */ - bind(key: string, desc: string, action: Function): void { - this.binds[key] = (key) => action(); - } - - /** - * Bind a key to a cheat action. - * - * The action will only be applied if cheat mode is activated. - */ - bindCheat(key: string, desc: string, action: Function): void { - this.bind(key, `Cheat: ${desc}`, () => { - if (this.cheat) { - this.game.displayMessage(`Cheat ! ${desc}`); - action(); - } - }); - } - - /** - * Apply a key press - */ - keyPress(key: string): void { - if (this.keyboard_callback) { - this.keyboard_callback(key); - } else if (this.binds[key]) { - this.binds[key](key); - } - } - - /** - * Grab the keyboard to receive next key presses. - * - * Release will happen if another grab is made, or if releaseKeyboard is called. - * - * *handle* is used to identify the grabber. - */ - grabKeyboard(handle: any, callback: KeyPressedCallback): void { - this.keyboard_grabber = handle; - this.keyboard_callback = callback; - } - - /** - * Release the keyboard. - */ - releaseKeyboard(handle: any): void { - if (handle === this.keyboard_grabber) { - this.keyboard_grabber = null; - this.keyboard_callback = null; - } - } - - /** - * Force the cursor out of currently hovered object - */ - private forceLeaveHovered() { - if (this.hovered && this.hovered.data) { - let pointer = this.hovered.data.get("pointer"); - if (pointer) { - this.hovered.emit("pointerout", pointer); - } - } - } - - /** - * Setup hover/click handlers on an UI element - * - * This is done in a way that should be compatible with touch-enabled screen - */ - setHoverClick(obj: UIButton | UIContainer | UIImage, enter: Function = nop, leave: Function = nop, click: Function = nop, hovertime = 300, holdtime = 600, sound = false): void { - let holdstart = Timer.nowMs(); - let enternext: Function | null = null; - let entercalled = false; - let cursorinside = false; - let leftbutton = false; - let destroyed = false; - - obj.setDataEnabled(); - - if (obj instanceof UIImage) { - obj.setInteractive(); - } else if (!(obj instanceof UIButton)) { - let bounds = obj.getBounds(); - bounds.x -= obj.x; - bounds.y -= obj.y; - obj.setInteractive({ - hitArea: bounds, - hitAreaCallback: Phaser.Geom.Rectangle.Contains, - }); - } - - let prevententer = () => { - if (enternext != null) { - Timer.global.cancel(enternext); - enternext = null; - return true; - } else { - return false; - } - }; - - let effectiveenter = () => { - if (!destroyed) { - enternext = null; - entercalled = true; - enter(); - } - } - - let effectiveleave = () => { - prevententer(); - if (entercalled) { - entercalled = false; - leave(); - } - } - - obj.on("destroy", () => { - destroyed = true; - effectiveleave(); - }); - - obj.on("pointerover", (pointer: Phaser.Input.Pointer) => { - if (destroyed || !UITools.isVisible(obj)) return; - - if (this.hovered) { - if (this.hovered === obj) { - return; - } else { - if (this.debug) { - console.log("Pointer force out", this.hovered); - } - this.forceLeaveHovered(); - } - } - this.hovered = obj; - this.hovered.data.set("pointer", pointer); - - if (this.debug) { - console.log("Pointer over", obj); - } - - cursorinside = true; - enternext = Timer.global.schedule(hovertime, effectiveenter); - }); - - obj.on("pointerout", () => { - if (destroyed) return; - - if (this.hovered === obj) { - this.hovered = null; - if (this.debug) { - console.log("Pointer out", obj); - } - } - - cursorinside = false; - effectiveleave(); - }); - - obj.on("pointerdown", (pointer?: Phaser.Input.Pointer) => { - if (destroyed || (pointer && pointer.buttons != 1)) return; - leftbutton = true; - - if (UITools.isVisible(obj)) { - holdstart = Timer.nowMs(); - if (sound) { - this.view.audio.playOnce("ui-button-down"); - } - if (!cursorinside && !enternext) { - enternext = Timer.global.schedule(holdtime, effectiveenter); - } - } - }); - - obj.on("pointerup", (pointer?: Phaser.Input.Pointer) => { - if (destroyed || !leftbutton) return; - leftbutton = false; - - if (!cursorinside) { - effectiveleave(); - } - - if (Timer.fromMs(holdstart) < holdtime) { - if (!cursorinside) { - effectiveenter(); - } - if (sound) { - this.view.audio.playOnce("ui-button-up"); - } - click(); - if (!cursorinside) { - effectiveleave(); - } - } - }); + this.forceLeaveHovered(); + + if (!contains(["Control", "Shift", "Alt", "Meta"], event.key)) { + this.keyPress(event.key); + if (event.code != event.key) { + this.keyPress(event.code); + } } + }); } + } + + /** + * Remove the bindings + */ + destroy(): void { + this.view.input.keyboard.removeAllListeners("keyup"); + } + + /** + * Bind a key to a specific action. + */ + bind(key: string, desc: string, action: Function): void { + this.binds[key] = (key) => action(); + } + + /** + * Bind a key to a cheat action. + * + * The action will only be applied if cheat mode is activated. + */ + bindCheat(key: string, desc: string, action: Function): void { + this.bind(key, `Cheat: ${desc}`, () => { + if (this.cheat) { + this.game.displayMessage(`Cheat ! ${desc}`); + action(); + } + }); + } + + /** + * Apply a key press + */ + keyPress(key: string): void { + if (this.keyboard_callback) { + this.keyboard_callback(key); + } else if (this.binds[key]) { + this.binds[key](key); + } + } + + /** + * Grab the keyboard to receive next key presses. + * + * Release will happen if another grab is made, or if releaseKeyboard is called. + * + * *handle* is used to identify the grabber. + */ + grabKeyboard(handle: any, callback: KeyPressedCallback): void { + this.keyboard_grabber = handle; + this.keyboard_callback = callback; + } + + /** + * Release the keyboard. + */ + releaseKeyboard(handle: any): void { + if (handle === this.keyboard_grabber) { + this.keyboard_grabber = null; + this.keyboard_callback = null; + } + } + + /** + * Force the cursor out of currently hovered object + */ + private forceLeaveHovered() { + if (this.hovered && this.hovered.data) { + let pointer = this.hovered.data.get("pointer"); + if (pointer) { + this.hovered.emit("pointerout", pointer); + } + } + } + + /** + * Setup hover/click handlers on an UI element + * + * This is done in a way that should be compatible with touch-enabled screen + */ + setHoverClick(obj: UIButton | UIContainer | UIImage, enter: Function = nop, leave: Function = nop, click: Function = nop, hovertime = 300, holdtime = 600, sound = false): void { + let holdstart = Timer.nowMs(); + let enternext: Function | null = null; + let entercalled = false; + let cursorinside = false; + let leftbutton = false; + let destroyed = false; + + obj.setDataEnabled(); + + if (obj instanceof UIImage) { + obj.setInteractive(); + } else if (!(obj instanceof UIButton)) { + let bounds = obj.getBounds(); + bounds.x -= obj.x; + bounds.y -= obj.y; + obj.setInteractive({ + hitArea: bounds, + hitAreaCallback: Phaser.Geom.Rectangle.Contains, + }); + } + + let prevententer = () => { + if (enternext != null) { + Timer.global.cancel(enternext); + enternext = null; + return true; + } else { + return false; + } + }; + + let effectiveenter = () => { + if (!destroyed) { + enternext = null; + entercalled = true; + enter(); + } + } + + let effectiveleave = () => { + prevententer(); + if (entercalled) { + entercalled = false; + leave(); + } + } + + obj.on("destroy", () => { + destroyed = true; + effectiveleave(); + }); + + obj.on("pointerover", (pointer: Phaser.Input.Pointer) => { + if (destroyed || !UITools.isVisible(obj)) return; + + if (this.hovered) { + if (this.hovered === obj) { + return; + } else { + if (this.debug) { + console.log("Pointer force out", this.hovered); + } + this.forceLeaveHovered(); + } + } + this.hovered = obj; + this.hovered.data.set("pointer", pointer); + + if (this.debug) { + console.log("Pointer over", obj); + } + + cursorinside = true; + enternext = Timer.global.schedule(hovertime, effectiveenter); + }); + + obj.on("pointerout", () => { + if (destroyed) return; + + if (this.hovered === obj) { + this.hovered = null; + if (this.debug) { + console.log("Pointer out", obj); + } + } + + cursorinside = false; + effectiveleave(); + }); + + obj.on("pointerdown", (pointer?: Phaser.Input.Pointer) => { + if (destroyed || (pointer && pointer.buttons != 1)) return; + leftbutton = true; + + if (UITools.isVisible(obj)) { + holdstart = Timer.nowMs(); + if (sound) { + this.view.audio.playOnce("ui-button-down"); + } + if (!cursorinside && !enternext) { + enternext = Timer.global.schedule(holdtime, effectiveenter); + } + } + }); + + obj.on("pointerup", (pointer?: Phaser.Input.Pointer) => { + if (destroyed || !leftbutton) return; + leftbutton = false; + + if (!cursorinside) { + effectiveleave(); + } + + if (Timer.fromMs(holdstart) < holdtime) { + if (!cursorinside) { + effectiveenter(); + } + if (sound) { + this.view.audio.playOnce("ui-button-up"); + } + click(); + if (!cursorinside) { + effectiveleave(); + } + } + }); + } } diff --git a/src/ui/common/Messages.ts b/src/ui/common/Messages.ts index 1fe3240..d2b66d2 100644 --- a/src/ui/common/Messages.ts +++ b/src/ui/common/Messages.ts @@ -1,62 +1,68 @@ -module TK.SpaceTac.UI { - /** - * A single displayed message - */ - class Message extends UIContainer { - view: BaseView - background: UIBackground - text: UIText +import { cfilter } from "../../common/Tools"; +import { BaseView } from "../BaseView"; +import { UIBackground } from "./UIBackground"; +import { UIBuilder } from "./UIBuilder"; +import { UIContainer } from "./UIContainer"; +import { UIText } from "./UIText"; +import { UITools } from "./UITools"; - constructor(parent: Messages, text: string, duration: number) { - super(parent.view); +/** + * A single displayed message + */ +class Message extends UIContainer { + view: BaseView + background: UIBackground + text: UIText - this.view = parent.view; - let builder = new UIBuilder(this.view).in(this); + constructor(parent: Messages, text: string, duration: number) { + super(parent.view); - this.background = new UIBackground(this.view, this); + this.view = parent.view; + let builder = new UIBuilder(this.view).in(this); - this.text = builder.text(text, 0, 0, { color: "#DBEFF9", shadow: true, size: 16, center: false, vcenter: false }); + this.background = new UIBackground(this.view, this); - UITools.drawBackground(this.text, this.background, 6); + this.text = builder.text(text, 0, 0, { color: "#DBEFF9", shadow: true, size: 16, center: false, vcenter: false }); - let bounds = UITools.getBounds(this); - this.setPosition(parent.view.getX(1) - bounds.width - 25, 25); - parent.view.timer.schedule(duration, () => this.hide()); - } + UITools.drawBackground(this.text, this.background, 6); - /** - * Hide the message - */ - hide() { - this.view.animations.addAnimation(this, { y: this.y + 50, alpha: 0 }, 400, "Circ.easeIn").then(() => this.destroy()); - } - } + let bounds = UITools.getBounds(this); + this.setPosition(parent.view.getX(1) - bounds.width - 25, 25); + parent.view.timer.schedule(duration, () => this.hide()); + } - /** - * Visual notifications of game-related messages (eg. "Game saved"...) - */ - export class Messages { - // Link to parent view - view: BaseView - - // Main group to hold the visual messages - container: UIContainer - - constructor(view: BaseView) { - this.view = view; - this.container = new UIBuilder(view, view.messages_layer).container("messages"); - } - - /** - * Add a new message to the notifications - */ - addMessage(text: string, duration: number = 3000): void { - let message = new Message(this, text, duration); - let bounds = UITools.getBounds(message); - cfilter(this.container.list, Message).forEach(child => { - child.y += bounds.height + 15; - }); - this.container.add(message); - } - } + /** + * Hide the message + */ + hide() { + this.view.animations.addAnimation(this, { y: this.y + 50, alpha: 0 }, 400, "Circ.easeIn").then(() => this.destroy()); + } +} + +/** + * Visual notifications of game-related messages (eg. "Game saved"...) + */ +export class Messages { + // Link to parent view + view: BaseView + + // Main group to hold the visual messages + container: UIContainer + + constructor(view: BaseView) { + this.view = view; + this.container = new UIBuilder(view, view.messages_layer).container("messages"); + } + + /** + * Add a new message to the notifications + */ + addMessage(text: string, duration: number = 3000): void { + let message = new Message(this, text, duration); + let bounds = UITools.getBounds(message); + cfilter(this.container.list, Message).forEach(child => { + child.y += bounds.height + 15; + }); + this.container.add(message); + } } diff --git a/src/ui/common/Tooltip.spec.ts b/src/ui/common/Tooltip.spec.ts index e6398c5..3e69050 100644 --- a/src/ui/common/Tooltip.spec.ts +++ b/src/ui/common/Tooltip.spec.ts @@ -1,30 +1,33 @@ -module TK.SpaceTac.UI.Specs { - testing("Tooltip", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { Tooltip } from "./Tooltip"; +import { UIBuilder } from "./UIBuilder"; - test.case("shows near the hovered button", check => { - let button = new UIBuilder(testgame.view).button("fake"); - check.patch(button, "getBounds", () => new Phaser.Geom.Rectangle(100, 50, 50, 25)); +testing("Tooltip", test => { + let testgame = setupEmptyView(test); - let tooltip = new Tooltip(testgame.view); - tooltip.bind(button, filler => true); + test.case("shows near the hovered button", check => { + let button = new UIBuilder(testgame.view).button("fake"); + check.patch(button, "getBounds", () => new Phaser.Geom.Rectangle(100, 50, 50, 25)); - let container = tooltip.container; - check.patch(container.content, "getBounds", () => new Phaser.Geom.Rectangle(0, 0, 32, 32)); - check.equals(container.visible, false); + let tooltip = new Tooltip(testgame.view); + tooltip.bind(button, filler => true); - let pointer = {}; - button.emit("pointerover", { pointer: pointer }); - check.equals(container.visible, false); + let container = tooltip.container; + check.patch(container.content, "getBounds", () => new Phaser.Geom.Rectangle(0, 0, 32, 32)); + check.equals(container.visible, false); - testgame.clockForward(1000); - container.update(); - check.equals(container.visible, true); - check.equals(container.x, 113); - check.equals(container.y, 91); + let pointer = {}; + button.emit("pointerover", { pointer: pointer }); + check.equals(container.visible, false); - button.emit("pointerout", { pointer: pointer }); - check.equals(container.visible, false); - }); - }); -} + testgame.clockForward(1000); + container.update(); + check.equals(container.visible, true); + check.equals(container.x, 113); + check.equals(container.y, 91); + + button.emit("pointerout", { pointer: pointer }); + check.equals(container.visible, false); + }); +}); diff --git a/src/ui/common/Tooltip.ts b/src/ui/common/Tooltip.ts index ee5cc1d..123a13f 100644 --- a/src/ui/common/Tooltip.ts +++ b/src/ui/common/Tooltip.ts @@ -1,202 +1,207 @@ -/// +import { cmp } from "../../common/Tools"; +import { MainUI } from "../../MainUI"; +import { BaseView } from "../BaseView"; +import { UIBackground } from "./UIBackground"; +import { UIBuilder, UITextStyle } from "./UIBuilder"; +import { UIButton } from "./UIButton"; +import { UIContainer } from "./UIContainer"; +import { UIImage } from "./UIImage"; +import { IBounded, UITools } from "./UITools"; -module TK.SpaceTac.UI { +export type TooltipFiller = string | ((filler: TooltipBuilder) => string) | ((filler: TooltipBuilder) => boolean); - export type TooltipFiller = string | ((filler: TooltipBuilder) => string) | ((filler: TooltipBuilder) => boolean); +export class TooltipContainer extends UIContainer { + view: BaseView + background: UIBackground + content: UIContainer + item?: IBounded + border = 10 + margin = 6 + viewport: IBounded | null = null - export class TooltipContainer extends UIContainer { - view: BaseView - background: UIBackground - content: UIContainer - item?: IBounded - border = 10 - margin = 6 - viewport: IBounded | null = null + constructor(view: BaseView) { + super(view); - constructor(view: BaseView) { - super(view); + this.view = view; + this.visible = false; - this.view = view; - this.visible = false; + this.background = new UIBackground(view, this); - this.background = new UIBackground(view, this); + this.content = new UIContainer(view); + this.add(this.content); - this.content = new UIContainer(view); - this.add(this.content); + this.view.tooltip_layer.add(this); + } - this.view.tooltip_layer.add(this); + show(item: IBounded) { + this.item = item; + this.visible = true; + this.update(); + } + + tryPosition(viewport: IBounded, tooltip: IBounded): [number, number, number] { + let [x, y] = UITools.positionInside(tooltip, viewport); + let distance = Math.max(Math.abs(x - tooltip.x), Math.abs(y - tooltip.y)); + if (this.view.isMouseInside({ x: x, y: y, width: tooltip.width, height: tooltip.height })) { + distance += 1000; + } + return [x, y, distance]; + } + + getBestPosition(item: IBounded, width: number, height: number): [number, number] { + let viewport = this.viewport || { + x: this.view.getX(0), + y: this.view.getY(0), + width: this.view.getX(1) - this.view.getX(0), + height: this.view.getY(1) - this.view.getY(0) + }; + let candidates = [ + this.tryPosition(viewport, { x: item.x + item.width / 2 - width / 2, y: item.y + item.height + this.margin, width: width, height: height }), + this.tryPosition(viewport, { x: item.x + item.width + this.margin, y: item.y + item.height / 2 - height / 2, width: width, height: height }), + this.tryPosition(viewport, { x: item.x + item.width / 2 - width / 2, y: item.y - height - this.margin, width: width, height: height }), + this.tryPosition(viewport, { x: item.x - width - this.margin, y: item.y + item.height / 2 - height / 2, width: width, height: height }), + ] + candidates[0][2] -= 1; // preference to down tooltip on equality + let [x, y, distance] = candidates.sort((a, b) => cmp(a[2], b[2]))[0]; + return [x, y]; + } + + update() { + if (this.visible && this.item) { + let [width, height] = UITools.drawBackground(this.content, this.background, this.border); + + let [x, y] = this.getBestPosition(this.item, width, height); + x += this.border; + y += this.border; + if (x != this.x || y != this.y) { + this.setPosition(x, y); + } + } + } + + hide() { + this.content.removeAll(); + this.background.clear(); + this.visible = false; + } +} + +/** + * Functions used to fill a tooltip content + */ +export class TooltipBuilder extends UIBuilder { + private content: TooltipContainer; + + constructor(container: TooltipContainer) { + let style = new UITextStyle(); + style.center = false; + style.vcenter = false; + style.shadow = true; + super(container.view, container.content, style); + + this.content = container; + } + + /** + * Configure the positioning and base style of the tooltip + */ + configure(border = 10, margin = 6, viewport: IBounded | null = null): void { + this.content.border = border; + this.content.margin = margin; + if (viewport) { + this.content.viewport = viewport; + } + } +} + +/** + * Tooltip system, to display information on hover + */ +export class Tooltip { + readonly view: BaseView; + readonly container: TooltipContainer; + + constructor(view: BaseView) { + this.view = view; + this.container = new TooltipContainer(view); + } + + get ui(): MainUI { + return this.view.gameui; + } + + /** + * Get a tooltip builder + */ + getBuilder(): TooltipBuilder { + return new TooltipBuilder(this.container); + } + + /** + * Bind to an UI component + * + * When the component is hovered, the function is called to allow filling the tooltip container + */ + bind(obj: UIButton | UIImage, func: (filler: TooltipBuilder) => boolean): void { + this.view.inputs.setHoverClick(obj, + // enter + () => { + this.hide(); + if (func(this.getBuilder())) { + this.container.show(UITools.getBounds(obj)); } + }, + // leave + () => this.hide(), + // click + () => this.hide() + ); + obj.on("pointerdown", () => this.hide()); + } - show(item: IBounded) { - this.item = item; - this.visible = true; - this.update(); - } + /** + * Bind to an UI component to display a dynamic text + */ + bindDynamicText(obj: UIButton | UIImage, text_getter: () => string): void { + this.bind(obj, filler => { + let content = text_getter(); + if (content) { + filler.text(content, 0, 0, { color: "#cccccc", size: 20 }); + return true; + } else { + return false; + } + }); + } - tryPosition(viewport: IBounded, tooltip: IBounded): [number, number, number] { - let [x, y] = UITools.positionInside(tooltip, viewport); - let distance = Math.max(Math.abs(x - tooltip.x), Math.abs(y - tooltip.y)); - if (this.view.isMouseInside({ x: x, y: y, width: tooltip.width, height: tooltip.height })) { - distance += 1000; - } - return [x, y, distance]; - } + /** + * Bind to an UI component to display a simple text + */ + bindStaticText(obj: UIButton | UIImage, text: string): void { + this.bindDynamicText(obj, () => text); + } - getBestPosition(item: IBounded, width: number, height: number): [number, number] { - let viewport = this.viewport || { - x: this.view.getX(0), - y: this.view.getY(0), - width: this.view.getX(1) - this.view.getX(0), - height: this.view.getY(1) - this.view.getY(0) - }; - let candidates = [ - this.tryPosition(viewport, { x: item.x + item.width / 2 - width / 2, y: item.y + item.height + this.margin, width: width, height: height }), - this.tryPosition(viewport, { x: item.x + item.width + this.margin, y: item.y + item.height / 2 - height / 2, width: width, height: height }), - this.tryPosition(viewport, { x: item.x + item.width / 2 - width / 2, y: item.y - height - this.margin, width: width, height: height }), - this.tryPosition(viewport, { x: item.x - width - this.margin, y: item.y + item.height / 2 - height / 2, width: width, height: height }), - ] - candidates[0][2] -= 1; // preference to down tooltip on equality - let [x, y, distance] = candidates.sort((a, b) => cmp(a[2], b[2]))[0]; - return [x, y]; - } - - update() { - if (this.visible && this.item) { - let [width, height] = UITools.drawBackground(this.content, this.background, this.border); - - let [x, y] = this.getBestPosition(this.item, width, height); - x += this.border; - y += this.border; - if (x != this.x || y != this.y) { - this.setPosition(x, y); - } - } - } - - hide() { - this.content.removeAll(); - this.background.clear(); - this.visible = false; - } + /** + * Show a tooltip for a component + */ + show(obj: UIButton, content: TooltipFiller): void { + let builder = this.getBuilder(); + let scontent = (typeof content == "string") ? content : content(builder); + if (typeof scontent == "string") { + builder.text(scontent, 0, 0, { color: "#cccccc", size: 20 }); } - /** - * Functions used to fill a tooltip content - */ - export class TooltipBuilder extends UIBuilder { - private content: TooltipContainer; - - constructor(container: TooltipContainer) { - let style = new UITextStyle(); - style.center = false; - style.vcenter = false; - style.shadow = true; - super(container.view, container.content, style); - - this.content = container; - } - - /** - * Configure the positioning and base style of the tooltip - */ - configure(border = 10, margin = 6, viewport: IBounded | null = null): void { - this.content.border = border; - this.content.margin = margin; - if (viewport) { - this.content.viewport = viewport; - } - } + if (scontent) { + this.container.show(UITools.getBounds(obj)); + } else { + this.hide(); } + } - /** - * Tooltip system, to display information on hover - */ - export class Tooltip { - readonly view: BaseView; - readonly container: TooltipContainer; - - constructor(view: BaseView) { - this.view = view; - this.container = new TooltipContainer(view); - } - - get ui(): MainUI { - return this.view.gameui; - } - - /** - * Get a tooltip builder - */ - getBuilder(): TooltipBuilder { - return new TooltipBuilder(this.container); - } - - /** - * Bind to an UI component - * - * When the component is hovered, the function is called to allow filling the tooltip container - */ - bind(obj: UIButton | UIImage, func: (filler: TooltipBuilder) => boolean): void { - this.view.inputs.setHoverClick(obj, - // enter - () => { - this.hide(); - if (func(this.getBuilder())) { - this.container.show(UITools.getBounds(obj)); - } - }, - // leave - () => this.hide(), - // click - () => this.hide() - ); - obj.on("pointerdown", () => this.hide()); - } - - /** - * Bind to an UI component to display a dynamic text - */ - bindDynamicText(obj: UIButton | UIImage, text_getter: () => string): void { - this.bind(obj, filler => { - let content = text_getter(); - if (content) { - filler.text(content, 0, 0, { color: "#cccccc", size: 20 }); - return true; - } else { - return false; - } - }); - } - - /** - * Bind to an UI component to display a simple text - */ - bindStaticText(obj: UIButton | UIImage, text: string): void { - this.bindDynamicText(obj, () => text); - } - - /** - * Show a tooltip for a component - */ - show(obj: UIButton, content: TooltipFiller): void { - let builder = this.getBuilder(); - let scontent = (typeof content == "string") ? content : content(builder); - if (typeof scontent == "string") { - builder.text(scontent, 0, 0, { color: "#cccccc", size: 20 }); - } - - if (scontent) { - this.container.show(UITools.getBounds(obj)); - } else { - this.hide(); - } - } - - /** - * Hide the current tooltip - */ - hide(): void { - this.container.hide(); - } - } -} \ No newline at end of file + /** + * Hide the current tooltip + */ + hide(): void { + this.container.hide(); + } +} diff --git a/src/ui/common/UIAwaiter.ts b/src/ui/common/UIAwaiter.ts index 1c098ae..3507e4d 100644 --- a/src/ui/common/UIAwaiter.ts +++ b/src/ui/common/UIAwaiter.ts @@ -1,26 +1,28 @@ -module TK.SpaceTac.UI { - /** - * UI component to show a loader animation while waiting for something - */ - export class UIAwaiter extends UIContainer { - constructor(view: BaseView, x: number, y: number, visible: boolean) { - super(view, x, y); - this.setName("awaiter"); - this.setVisible(visible); +import { BaseView } from "../BaseView"; +import { UIContainer } from "./UIContainer"; +import { FacingAlwaysParticle, UIParticles } from "./UIParticles"; - let manager = new UIParticles(view).createManager("common-awaiter", this); - let circle = new Phaser.Geom.Circle(0, 0, 60); - manager.createEmitter({ - angle: { start: 0, end: 360, steps: 6 }, - alpha: { start: 1, end: 0, ease: "Quad.easeIn" }, - lifespan: 1200, - speed: 5, - quantity: 1, - scale: { start: 0.9, end: 1, ease: "Quad.easeOut" }, - frequency: 200, - particleClass: FacingAlwaysParticle, - emitZone: { type: 'edge', source: circle, quantity: 6 } - }); - } - } +/** + * UI component to show a loader animation while waiting for something + */ +export class UIAwaiter extends UIContainer { + constructor(view: BaseView, x: number, y: number, visible: boolean) { + super(view, x, y); + this.setName("awaiter"); + this.setVisible(visible); + + let manager = new UIParticles(view).createManager("common-awaiter", this); + let circle = new Phaser.Geom.Circle(0, 0, 60); + manager.createEmitter({ + angle: { start: 0, end: 360, steps: 6 }, + alpha: { start: 1, end: 0, ease: "Quad.easeIn" }, + lifespan: 1200, + speed: 5, + quantity: 1, + scale: { start: 0.9, end: 1, ease: "Quad.easeOut" }, + frequency: 200, + particleClass: FacingAlwaysParticle, + emitZone: { type: 'edge', source: circle, quantity: 6 } + }); + } } diff --git a/src/ui/common/UIBackground.ts b/src/ui/common/UIBackground.ts index a873a65..f3245a1 100644 --- a/src/ui/common/UIBackground.ts +++ b/src/ui/common/UIBackground.ts @@ -1,59 +1,64 @@ -module TK.SpaceTac.UI { - /** - * Decorated background for dynamic sized content (such as tooltips) - */ - export class UIBackground { - private graphics: UIGraphics - x = 0 - y = 0 - width = 0 - height = 0 +import { BaseView } from "../BaseView" +import { UIBuilder } from "./UIBuilder" +import { UIContainer } from "./UIContainer" +import { UIGraphics } from "./UIGraphics" +import { UIText } from "./UIText" +import { UITools } from "./UITools" - constructor(readonly view: BaseView, readonly parent: UIContainer, readonly border = 6) { - this.graphics = new UIBuilder(view, parent).graphics("background", 0, 0, false); - } +/** + * Decorated background for dynamic sized content (such as tooltips) + */ +export class UIBackground { + private graphics: UIGraphics + x = 0 + y = 0 + width = 0 + height = 0 - /** - * Adapt the background to cover a given content - */ - adaptToContent(content: UIContainer | UIText): void { - if (content.parentContainer != this.graphics.parentContainer) { - console.error("Content and background should have the same parent container"); - return; - } + constructor(readonly view: BaseView, readonly parent: UIContainer, readonly border = 6) { + this.graphics = new UIBuilder(view, parent).graphics("background", 0, 0, false); + } - let bounds = UITools.getBounds(content); - - let x = bounds.x - this.graphics.parentContainer.x - this.border; - let y = bounds.y - this.graphics.parentContainer.y - this.border; - let width = bounds.width + 2 * this.border; - let height = bounds.height + 2 * this.border; - - if (x != this.x || y != this.y || width != this.width || height != this.height) { - this.graphics.clear(); - this.graphics.lineStyle(2, 0x6690a4); - this.graphics.fillStyle(0x162730); - this.graphics.fillRect(x, y, width, height); - this.graphics.strokeRect(x, y, width, height); - this.graphics.setVisible(true); - - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - /** - * Remove the drawn background - */ - clear(): void { - this.graphics.setVisible(false); - this.graphics.clear(); - this.x = 0; - this.y = 0; - this.width = 0; - this.height = 0; - } + /** + * Adapt the background to cover a given content + */ + adaptToContent(content: UIContainer | UIText): void { + if (content.parentContainer != this.graphics.parentContainer) { + console.error("Content and background should have the same parent container"); + return; } + + let bounds = UITools.getBounds(content); + + let x = bounds.x - this.graphics.parentContainer.x - this.border; + let y = bounds.y - this.graphics.parentContainer.y - this.border; + let width = bounds.width + 2 * this.border; + let height = bounds.height + 2 * this.border; + + if (x != this.x || y != this.y || width != this.width || height != this.height) { + this.graphics.clear(); + this.graphics.lineStyle(2, 0x6690a4); + this.graphics.fillStyle(0x162730); + this.graphics.fillRect(x, y, width, height); + this.graphics.strokeRect(x, y, width, height); + this.graphics.setVisible(true); + + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + } + + /** + * Remove the drawn background + */ + clear(): void { + this.graphics.setVisible(false); + this.graphics.clear(); + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + } } diff --git a/src/ui/common/UIBuilder.spec.ts b/src/ui/common/UIBuilder.spec.ts index b3e49cf..ffb25e2 100644 --- a/src/ui/common/UIBuilder.spec.ts +++ b/src/ui/common/UIBuilder.spec.ts @@ -1,278 +1,286 @@ -module TK.SpaceTac.UI.Specs { - testing("UIBuilder", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { iteritems } from "../../common/Tools"; +import { setupEmptyView, testClick } from "../TestGame"; +import { UIBuilder, UITextStyle } from "./UIBuilder"; +import { UIButton } from "./UIButton"; +import { UIContainer } from "./UIContainer"; +import { UIImage } from "./UIImage"; +import { UIText } from "./UIText"; +import { UITools } from "./UITools"; - function get(path: (number | string)[]): [string, any] { - let spath = `[${path.join(" -> ")}]`; - let component: Phaser.GameObjects.GameObject | Phaser.Scene | null = testgame.view; - path.forEach(idx => { - if (component instanceof Phaser.Scene) { - component = (typeof idx == "number") ? component.children.list[idx] : component.children.getByName(idx); - } else if (component instanceof Phaser.GameObjects.Container) { - component = (typeof idx == "number") ? component.list[idx] : component.getByName(idx); - } else { - component = null; - } - if (!component) { - throw new Error(`Path not found: ${spath} (${idx} part)`); - } - }); - return [spath, component]; - } +testing("UIBuilder", test => { + let testgame = setupEmptyView(test); - function checkcomp(path: (number | string)[], ctype?: { new(...args: any[]): T }, name?: string, attrs?: Partial): T { - let [spath, component] = get(path); + function get(path: (number | string)[]): [string, any] { + let spath = `[${path.join(" -> ")}]`; + let component: Phaser.GameObjects.GameObject | Phaser.Scene | null = testgame.view; + path.forEach(idx => { + if (component instanceof Phaser.Scene) { + component = (typeof idx == "number") ? component.children.list[idx] : component.children.getByName(idx); + } else if (component instanceof Phaser.GameObjects.Container) { + component = (typeof idx == "number") ? component.list[idx] : component.getByName(idx); + } else { + component = null; + } + if (!component) { + throw new Error(`Path not found: ${spath} (${idx} part)`); + } + }); + return [spath, component]; + } - if (typeof ctype != "undefined") { - test.check.same(component instanceof ctype, true, `${spath} is not of good type`); - } - if (typeof name != "undefined") { - test.check.equals(component.name, name, spath); - } - if (typeof attrs != "undefined") { - iteritems(attrs, (key, value) => { - test.check.equals(component[key], value, spath); - }); - } + function checkcomp(path: (number | string)[], ctype?: { new(...args: any[]): T }, name?: string, attrs?: Partial): T { + let [spath, component] = get(path); - return component; - } + if (typeof ctype != "undefined") { + test.check.same(component instanceof ctype, true, `${spath} is not of good type`); + } + if (typeof name != "undefined") { + test.check.equals(component.name, name, spath); + } + if (typeof attrs != "undefined") { + iteritems(attrs, (key, value) => { + test.check.equals(component[key], value, spath); + }); + } - function checktext(path: (number | string)[], attrs?: Partial, style?: Partial): UIText { - let text = checkcomp(path, UIText, "", attrs); + return component; + } - if (typeof style != "undefined") { - iteritems(style, (key, value) => { - test.check.equals((text.style)[key], value, `text style ${key}`); - }); - } + function checktext(path: (number | string)[], attrs?: Partial, style?: Partial): UIText { + let text = checkcomp(path, UIText, "", attrs); - return text; - } + if (typeof style != "undefined") { + iteritems(style, (key, value) => { + test.check.equals((text.style)[key], value, `text style ${key}`); + }); + } - test.case("can work on view layers", check => { - let builder = new UIBuilder(testgame.view, "tl1"); - builder.container("tg1"); - checkcomp(["View layers", "tl1", 0], UIContainer, "tg1"); + return text; + } - builder = new UIBuilder(testgame.view, "tl2"); - builder.container("tg2"); - checkcomp(["View layers", "tl2", 0], UIContainer, "tg2"); + test.case("can work on view layers", check => { + let builder = new UIBuilder(testgame.view, "tl1"); + builder.container("tg1"); + checkcomp(["View layers", "tl1", 0], UIContainer, "tg1"); - builder = new UIBuilder(testgame.view, "tl1"); - builder.container("tg3"); - checkcomp(["View layers", "tl1", 0], UIContainer, "tg1"); - checkcomp(["View layers", "tl1", 1], UIContainer, "tg3"); + builder = new UIBuilder(testgame.view, "tl2"); + builder.container("tg2"); + checkcomp(["View layers", "tl2", 0], UIContainer, "tg2"); - builder = new UIBuilder(testgame.view); - builder.container("tg4"); - checkcomp(["View layers", "base", 0], UIContainer, "tg4"); + builder = new UIBuilder(testgame.view, "tl1"); + builder.container("tg3"); + checkcomp(["View layers", "tl1", 0], UIContainer, "tg1"); + checkcomp(["View layers", "tl1", 1], UIContainer, "tg3"); - builder = new UIBuilder(testgame.view); - builder.container("tg5"); - checkcomp(["View layers", "base", 0], UIContainer, "tg4"); - checkcomp(["View layers", "base", 1], UIContainer, "tg5"); + builder = new UIBuilder(testgame.view); + builder.container("tg4"); + checkcomp(["View layers", "base", 0], UIContainer, "tg4"); - check.equals(testgame.view.layers.list.map((child: any) => child.name), ["tl1", "tl2", "base"]); - }) + builder = new UIBuilder(testgame.view); + builder.container("tg5"); + checkcomp(["View layers", "base", 0], UIContainer, "tg4"); + checkcomp(["View layers", "base", 1], UIContainer, "tg5"); - test.case("creates component inside the parent container", check => { - let builder = new UIBuilder(testgame.view, testgame.view.getLayer("testlayer")); - let group = builder.container("test1"); - checkcomp(["View layers", "testlayer", 0], UIContainer, "test1"); + check.equals(testgame.view.layers.list.map((child: any) => child.name), ["tl1", "tl2", "base"]); + }) - builder = new UIBuilder(testgame.view, group); - builder.text("test2"); - checkcomp(["View layers", "testlayer", 0, 0], UIText, "", { text: "test2", parentContainer: group }); + test.case("creates component inside the parent container", check => { + let builder = new UIBuilder(testgame.view, testgame.view.getLayer("testlayer")); + let group = builder.container("test1"); + checkcomp(["View layers", "testlayer", 0], UIContainer, "test1"); - builder = new UIBuilder(testgame.view, "anothertestlayer"); - builder.text("test3"); - checkcomp(["View layers", "anothertestlayer", 0], UIText, "", { text: "test3" }); - }) + builder = new UIBuilder(testgame.view, group); + builder.text("test2"); + checkcomp(["View layers", "testlayer", 0, 0], UIText, "", { text: "test2", parentContainer: group }); - test.case("can clear a container", check => { - let builder = new UIBuilder(testgame.view); - builder.container("group1", 50, 30); - builder.text("text1"); - let [spath, container] = get(["View layers", "base"]); - if (check.instance(container, UIContainer, "is a container")) { - check.equals(container.list.length, 2); - builder.clear(); - check.equals(container.list.length, 0); - } - }) + builder = new UIBuilder(testgame.view, "anothertestlayer"); + builder.text("test3"); + checkcomp(["View layers", "anothertestlayer", 0], UIText, "", { text: "test3" }); + }) - test.case("can create containers", check => { - let builder = new UIBuilder(testgame.view); - builder.container("group1", 50, 30); - checkcomp(["View layers", "base", 0], UIContainer, "group1", { x: 50, y: 30 }); - }) + test.case("can clear a container", check => { + let builder = new UIBuilder(testgame.view); + builder.container("group1", 50, 30); + builder.text("text1"); + let [spath, container] = get(["View layers", "base"]); + if (check.instance(container, UIContainer, "is a container")) { + check.equals(container.list.length, 2); + builder.clear(); + check.equals(container.list.length, 0); + } + }) - test.case("can create texts", check => { - let builder = new UIBuilder(testgame.view); - builder.text("Test content", 12, 41); - checktext(["View layers", "base", 0], { text: "Test content", x: 12, y: 41 }); + test.case("can create containers", check => { + let builder = new UIBuilder(testgame.view); + builder.container("group1", 50, 30); + checkcomp(["View layers", "base", 0], UIContainer, "group1", { x: 50, y: 30 }); + }) - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { size: 61 }); - checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt" }); - checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "61pt" }); + test.case("can create texts", check => { + let builder = new UIBuilder(testgame.view); + builder.text("Test content", 12, 41); + checktext(["View layers", "base", 0], { text: "Test content", x: 12, y: 41 }); - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { color: "#252627" }); - checktext(["View layers", "base", 0], undefined, { color: "#ffffff" }); - checktext(["View layers", "base", 1], undefined, { color: "#252627" }); + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { size: 61 }); + checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt" }); + checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "61pt" }); - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { shadow: true }); - checktext(["View layers", "base", 0], undefined, { shadowColor: "#000", shadowFill: false, shadowStroke: false }); - checktext(["View layers", "base", 1], undefined, { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 3, shadowStroke: true }); + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { color: "#252627" }); + checktext(["View layers", "base", 0], undefined, { color: "#ffffff" }); + checktext(["View layers", "base", 1], undefined, { color: "#252627" }); - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { stroke_width: 2, stroke_color: "#ff0000" }); - checktext(["View layers", "base", 0], undefined, { stroke: "#fff", strokeThickness: 0 }); - checktext(["View layers", "base", 1], undefined, { stroke: "#ff0000", strokeThickness: 2 }); + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { shadow: true }); + checktext(["View layers", "base", 0], undefined, { shadowColor: "#000", shadowFill: false, shadowStroke: false }); + checktext(["View layers", "base", 1], undefined, { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 3, shadowStroke: true }); - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { bold: true }); - checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); - checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { stroke_width: 2, stroke_color: "#ff0000" }); + checktext(["View layers", "base", 0], undefined, { stroke: "#fff", strokeThickness: 0 }); + checktext(["View layers", "base", 1], undefined, { stroke: "#ff0000", strokeThickness: 2 }); - builder.clear(); - builder.text("", 0, 0, {}); - builder.text("", 0, 0, { center: false }); - builder.text("", 0, 0, { vcenter: false }); - builder.text("", 0, 0, { center: false, vcenter: false }); - checktext(["View layers", "base", 0], { originX: 0.5, originY: 0.5 }, { align: "center" }); - checktext(["View layers", "base", 1], { originX: 0, originY: 0.5 }, { align: "left" }); - checktext(["View layers", "base", 2], { originX: 0.5, originY: 0 }, { align: "center" }); - checktext(["View layers", "base", 3], { originX: 0, originY: 0 }, { align: "left" }); + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { bold: true }); + checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); + checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); - builder.clear(); - builder.text("", 0, 0, { width: 0 }); - builder.text("", 0, 0, { width: 1100 }); - checktext(["View layers", "base", 0], undefined, { wordWrapWidth: null }); - checktext(["View layers", "base", 1], undefined, { wordWrapWidth: 1100 }); - }) + builder.clear(); + builder.text("", 0, 0, {}); + builder.text("", 0, 0, { center: false }); + builder.text("", 0, 0, { vcenter: false }); + builder.text("", 0, 0, { center: false, vcenter: false }); + checktext(["View layers", "base", 0], { originX: 0.5, originY: 0.5 }, { align: "center" }); + checktext(["View layers", "base", 1], { originX: 0, originY: 0.5 }, { align: "left" }); + checktext(["View layers", "base", 2], { originX: 0.5, originY: 0 }, { align: "center" }); + checktext(["View layers", "base", 3], { originX: 0, originY: 0 }, { align: "left" }); - test.case("can create images", check => { - let builder = new UIBuilder(testgame.view); - builder.image("test-image", 100, 50); - checkcomp(["View layers", "base", 0], UIImage, "test-image", { x: 100, y: 50 }); + builder.clear(); + builder.text("", 0, 0, { width: 0 }); + builder.text("", 0, 0, { width: 1100 }); + checktext(["View layers", "base", 0], undefined, { wordWrapWidth: null }); + checktext(["View layers", "base", 1], undefined, { wordWrapWidth: 1100 }); + }) - check.patch(testgame.view, "getFirstImage", (...images: string[]) => images[1]); - builder.image(["test-image1", "test-image2", "test-image3"]); - checkcomp(["View layers", "base", 1], UIImage, "test-image2"); - }) + test.case("can create images", check => { + let builder = new UIBuilder(testgame.view); + builder.image("test-image", 100, 50); + checkcomp(["View layers", "base", 0], UIImage, "test-image", { x: 100, y: 50 }); - test.case("can create buttons", check => { - let builder = new UIBuilder(testgame.view); - let a = 1; - let button1 = builder.button("test-image1", 100, 50, () => a += 1); - checkcomp(["View layers", "base", 0], UIButton, "test-image1", { x: 100, y: 50 }); - let button2 = builder.button("test-image2", 20, 10); - checkcomp(["View layers", "base", 1], UIButton, "test-image2", { x: 20, y: 10 }); + check.patch(testgame.view, "getFirstImage", (...images: string[]) => images[1]); + builder.image(["test-image1", "test-image2", "test-image3"]); + checkcomp(["View layers", "base", 1], UIImage, "test-image2"); + }) - check.equals(a, 1); - testClick(button1); - check.equals(a, 2); - testClick(button2); - check.equals(a, 2); - testClick(button1); - check.equals(a, 3); - }) + test.case("can create buttons", check => { + let builder = new UIBuilder(testgame.view); + let a = 1; + let button1 = builder.button("test-image1", 100, 50, () => a += 1); + checkcomp(["View layers", "base", 0], UIButton, "test-image1", { x: 100, y: 50 }); + let button2 = builder.button("test-image2", 20, 10); + checkcomp(["View layers", "base", 1], UIButton, "test-image2", { x: 20, y: 10 }); - test.case("creates sub-builders, preserving text style", check => { - let base_style = new UITextStyle(); - base_style.width = 123; - let builder = new UIBuilder(testgame.view, undefined, base_style); - builder.text("Test 1"); + check.equals(a, 1); + testClick(button1); + check.equals(a, 2); + testClick(button2); + check.equals(a, 2); + testClick(button1); + check.equals(a, 3); + }) - let group = builder.container("testgroup"); - let subbuilder = builder.in(group); - subbuilder.text("Test 2"); + test.case("creates sub-builders, preserving text style", check => { + let base_style = new UITextStyle(); + base_style.width = 123; + let builder = new UIBuilder(testgame.view, undefined, base_style); + builder.text("Test 1"); - checktext(["View layers", "base", 0], { text: "Test 1" }, { wordWrapWidth: 123 }); - checktext(["View layers", "base", 1, 0], { text: "Test 2" }, { wordWrapWidth: 123 }); - }) + let group = builder.container("testgroup"); + let subbuilder = builder.in(group); + subbuilder.text("Test 2"); - test.case("allows to alter text style", check => { - let builder = new UIBuilder(testgame.view); - builder.text("t1"); - builder.styled({ bold: true }).text("t2"); - builder.text("t3"); - builder.text("t4", undefined, undefined, { bold: true }); + checktext(["View layers", "base", 0], { text: "Test 1" }, { wordWrapWidth: 123 }); + checktext(["View layers", "base", 1, 0], { text: "Test 2" }, { wordWrapWidth: 123 }); + }) - checktext(["View layers", "base", 0], { text: "t1" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); - checktext(["View layers", "base", 1], { text: "t2" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); - checktext(["View layers", "base", 2], { text: "t3" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); - checktext(["View layers", "base", 3], { text: "t4" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); - }) + test.case("allows to alter text style", check => { + let builder = new UIBuilder(testgame.view); + builder.text("t1"); + builder.styled({ bold: true }).text("t2"); + builder.text("t3"); + builder.text("t4", undefined, undefined, { bold: true }); - test.case("allows to change text or image content", check => { - let builder = new UIBuilder(testgame.view); - let text = builder.text("test-text"); - let image = builder.image("test-image"); + checktext(["View layers", "base", 0], { text: "t1" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); + checktext(["View layers", "base", 1], { text: "t2" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); + checktext(["View layers", "base", 2], { text: "t3" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" }); + checktext(["View layers", "base", 3], { text: "t4" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" }); + }) - checkcomp(["View layers", "base", 0], UIText, "", { text: "test-text" }); - checkcomp(["View layers", "base", 1], UIImage, "test-image"); + test.case("allows to change text or image content", check => { + let builder = new UIBuilder(testgame.view); + let text = builder.text("test-text"); + let image = builder.image("test-image"); - builder.change(text, "test-mod-text"); - builder.change(image, "test-mod-image"); + checkcomp(["View layers", "base", 0], UIText, "", { text: "test-text" }); + checkcomp(["View layers", "base", 1], UIImage, "test-image"); - checkcomp(["View layers", "base", 0], UIText, "", { text: "test-mod-text" }); - checkcomp(["View layers", "base", 1], UIImage, "test-mod-image"); - }) + builder.change(text, "test-mod-text"); + builder.change(image, "test-mod-image"); - test.case("distributes children along an axis", check => { - let builder = new UIBuilder(testgame.view); - builder = builder.in(builder.container("test")); + checkcomp(["View layers", "base", 0], UIText, "", { text: "test-mod-text" }); + checkcomp(["View layers", "base", 1], UIImage, "test-mod-image"); + }) - let c1 = builder.text(""); - let c2 = builder.button("test"); - let c3 = builder.container("test"); + test.case("distributes children along an axis", check => { + let builder = new UIBuilder(testgame.view); + builder = builder.in(builder.container("test")); - check.equals(c1.x, 0); - check.equals(c1.y, 0); - check.equals(c2.x, 0); - check.equals(c2.y, 0); - check.equals(c3.x, 0); - check.equals(c3.y, 0); + let c1 = builder.text(""); + let c2 = builder.button("test"); + let c3 = builder.container("test"); - check.patch(UITools, "getBounds", (obj: any) => { - if (obj === c1) { - return { x: 0, y: 0, width: 100, height: 51 }; - } else if (obj === c2) { - return { x: 0, y: 0, width: 20, height: 7 }; - } else if (obj === c3) { - return { x: 0, y: 0, width: 60, height: 11 }; - } else { - return { x: 0, y: 0, width: 0, height: 0 }; - } - }); + check.equals(c1.x, 0); + check.equals(c1.y, 0); + check.equals(c2.x, 0); + check.equals(c2.y, 0); + check.equals(c3.x, 0); + check.equals(c3.y, 0); - builder.distribute("x", 100, 400); + check.patch(UITools, "getBounds", (obj: any) => { + if (obj === c1) { + return { x: 0, y: 0, width: 100, height: 51 }; + } else if (obj === c2) { + return { x: 0, y: 0, width: 20, height: 7 }; + } else if (obj === c3) { + return { x: 0, y: 0, width: 60, height: 11 }; + } else { + return { x: 0, y: 0, width: 0, height: 0 }; + } + }); - check.equals(c1.x, 130); - check.equals(c1.y, 0); - check.equals(c2.x, 260); - check.equals(c2.y, 0); - check.equals(c3.x, 310); - check.equals(c3.y, 0); + builder.distribute("x", 100, 400); - builder.distribute("y", 60, 180); + check.equals(c1.x, 130); + check.equals(c1.y, 0); + check.equals(c2.x, 260); + check.equals(c2.y, 0); + check.equals(c3.x, 310); + check.equals(c3.y, 0); - check.equals(c1.x, 130); - check.equals(c1.y, 73); - check.equals(c2.x, 260); - check.equals(c2.y, 137); - check.equals(c3.x, 310); - check.equals(c3.y, 156); - }) - }) -} + builder.distribute("y", 60, 180); + + check.equals(c1.x, 130); + check.equals(c1.y, 73); + check.equals(c2.x, 260); + check.equals(c2.y, 137); + check.equals(c3.x, 310); + check.equals(c3.y, 156); + }) +}) diff --git a/src/ui/common/UIBuilder.ts b/src/ui/common/UIBuilder.ts index 6fe1891..fa9afce 100644 --- a/src/ui/common/UIBuilder.ts +++ b/src/ui/common/UIBuilder.ts @@ -1,282 +1,292 @@ +import { merge, sum } from "../../common/Tools" +import { MainUI } from "../../MainUI" +import { BaseView } from "../BaseView" +import { TooltipFiller } from "./Tooltip" +import { UIAwaiter } from "./UIAwaiter" +import { UIButton, UIButtonOptions } from "./UIButton" +import { UIContainer } from "./UIContainer" +import { UIGraphics } from "./UIGraphics" +import { UIImage } from "./UIImage" +import { UIOverlay, UIOverlayOptions } from "./UIOverlay" +import { ParticlesConfig } from "./UIParticles" +import { UIText } from "./UIText" +import { UITools } from "./UITools" +import { ValueBar, ValueBarOrientation } from "./ValueBar" + +export type UIBuilderParent = UIImage | UIContainer + +export type ShaderValue = number | { x: number, y: number } +export type UIOnOffCallback = (on: boolean) => boolean + /** - * Main way to create UI components + * Text style interface */ -module TK.SpaceTac.UI { - export type UIBuilderParent = UIImage | UIContainer - - export type ShaderValue = number | { x: number, y: number } - export type UIOnOffCallback = (on: boolean) => boolean - - /** - * Text style interface - */ - export interface UITextStyleI { - size?: number - color?: string - shadow?: boolean - stroke_width?: number - stroke_color?: string - bold?: boolean - center?: boolean - vcenter?: boolean - width?: number - } - - /** - * Text style - */ - export class UITextStyle implements UITextStyleI { - // Size in points - size = 16 - - // Font color - color = "#ffffff" - - // Shadow under the text - shadow = false - - // Stroke around the letters - stroke_width = 0 - stroke_color = "#ffffff" - - // Bold text - bold = false - - // Centering - center = true - vcenter = true - - // Word wrapping - width = 0 - } - - /** - * Main UI builder tool - */ - export class UIBuilder { - view: BaseView - private game: MainUI - private parent: UIBuilderParent - private text_style: UITextStyleI - - constructor(view: BaseView, parent: UIBuilderParent | string = "base", text_style: UITextStyleI = new UITextStyle) { - this.view = view; - this.game = view.gameui; - if (typeof parent == "string") { - this.parent = view.getLayer(parent); - } else { - this.parent = parent; - } - this.text_style = text_style; - } - - /** - * Create a new UIBuilder inside a parent container, or a view layer - * - * This new builder will inherit the style settings, and will create components in the specified parent - */ - in(container: UIBuilderParent | string, body?: (builder: UIBuilder) => void): UIBuilder { - let result = new UIBuilder(this.view, container, this.text_style); - if (body) { - body(result); - } - return result; - } - - /** - * Create a new UIBuilder with style changes - */ - styled(changes: UITextStyleI, body?: (builder: UIBuilder) => void): UIBuilder { - let result = new UIBuilder(this.view, this.parent, merge(this.text_style, changes)); - if (body) { - body(result); - } - return result; - } - - /** - * Clear the current container of all component - */ - clear(): void { - if (this.parent instanceof UIImage) { - console.error("Cannot clear an image parent, use groups instead"); - } else { - this.parent.removeAll(true); - } - } - - /** - * Internal method to add to the parent - */ - private add(child: UIText | UIImage | UIButton | UIContainer | UIGraphics): void { - if (this.parent instanceof UIImage) { - let gparent = this.parent.parentContainer; - if (gparent) { - let x = this.parent.x + child.x; - let y = this.parent.y + child.y; - child.setPosition(x, y); - gparent.add(child); - } else { - throw new Error("no parent container"); - } - } else { - this.parent.add(child); - } - } - - /** - * Add a container of other components - */ - container(name: string, x = 0, y = 0, visible = true): UIContainer { - let result = new UIContainer(this.view, x, y); - result.setName(name); - result.setVisible(visible); - this.add(result); - return result; - } - - /** - * Add a text - * - * Anchor will be defined according to the style centering - */ - text(content: string, x = 0, y = 0, style_changes: UITextStyleI = {}): UIText { - let style = merge(this.text_style, style_changes); - let result = new UIText(this.view, x, y, content, { - fill: style.color, - align: style.center ? "center" : "left" - }); - result.setFont(`${style.bold ? "bold " : ""}${style.size}pt SpaceTac`); - result.setOrigin(style.center ? 0.5 : 0, style.vcenter ? 0.5 : 0); - if (style.width) { - result.setWordWrapWidth(style.width); - } - if (style.shadow) { - result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3, true, true); - } - if (style.stroke_width && style.stroke_color) { - result.setStroke(style.stroke_color, style.stroke_width); - } - this.add(result); - return result; - } - - /** - * Add an image - */ - image(name: string | string[], x = 0, y = 0, centered = false): UIImage { - if (typeof name != "string") { - name = this.view.getFirstImage(...name); - } - - let info = this.view.getImageInfo(name); - let result = new UIImage(this.view, x, y, info.key, info.frame); - result.name = name; - if (!centered) { - result.setOrigin(0); - } - this.add(result); - return result; - } - - /** - * Add a hoverable and/or clickable button - * - * If an image with "-hover" suffix is found in atlases, it will be used as hover mask (added as button child) - */ - button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}): UIButton { - options.text_style = merge(this.text_style, options.text_style || {}); - let result = new UIButton(this.view, name, x, y, onclick, tooltip, onoffcallback, options); - this.add(result); - return result; - } - - /** - * Add a value bar - */ - valuebar(name: string, x = 0, y = 0, orientation = ValueBarOrientation.EAST): ValueBar { - let result = new ValueBar(this.view, name, orientation, x, y); - this.add(result.node); - return result; - } - - /** - * Add a graphics (for drawing) - */ - graphics(name: string, x = 0, y = 0, visible = true): UIGraphics { - let result = new UIGraphics(this.view, name, visible, x, y); - this.add(result); - return result; - } - - /** - * Emit a bunch of particles - */ - particles(config: ParticlesConfig): void { - this.view.particles.emit(config, this.parent instanceof UIContainer ? this.parent : undefined); - } - - /** - * Animation to await something - */ - awaiter(x = 0, y = 0, visible = true, scale = 1): UIAwaiter { - let result = new UIAwaiter(this.view, x, y, visible); - result.setScale(scale); - this.add(result); - return result; - } - - /** - * Add a full-view capturing overlay - */ - overlay(options: UIOverlayOptions): UIOverlay { - let result = new UIOverlay(this.view, options); - this.add(result); - return result; - } - - /** - * Change the content of an component - * - * If the component is a text, its content will be changed. - * If the component is an image, its texture will be changed. - */ - change(component: UIImage | UIText, content: string): void { - // TODO Should be moved custom UIImage and UIText classes - if (component instanceof UIText) { - component.setText(content); - } else { - let info = this.view.getImageInfo(content); - component.setName(content); - component.setTexture(info.key, info.frame); - } - } - - /** - * Evenly distribute the children of this builder along an axis - */ - distribute(along: "x" | "y", start: number, end: number): void { - if (!(this.parent instanceof UIContainer)) { - throw new Error("UIBuilder.distribute only works on groups"); - } - let children = this.parent.list; - - let sizes = children.map(child => { - if (UITools.isSpatial(child)) { - return UITools.getBounds(child)[along == "x" ? "width" : "height"]; - } else { - return 0; - } - }); - let spacing = ((end - start) - sum(sizes)) / (sizes.length + 1); - let offset = start; - children.forEach((child, idx) => { - offset += spacing; - if (UITools.isSpatial(child)) { - child[along] = Math.round(offset); - } - offset += sizes[idx]; - }); - } - } +export interface UITextStyleI { + size?: number + color?: string + shadow?: boolean + stroke_width?: number + stroke_color?: string + bold?: boolean + center?: boolean + vcenter?: boolean + width?: number +} + +/** + * Text style + */ +export class UITextStyle implements UITextStyleI { + // Size in points + size = 16 + + // Font color + color = "#ffffff" + + // Shadow under the text + shadow = false + + // Stroke around the letters + stroke_width = 0 + stroke_color = "#ffffff" + + // Bold text + bold = false + + // Centering + center = true + vcenter = true + + // Word wrapping + width = 0 +} + +/** + * Main UI builder tool + */ +export class UIBuilder { + view: BaseView + private game: MainUI + private parent: UIBuilderParent + private text_style: UITextStyleI + + constructor(view: BaseView, parent: UIBuilderParent | string = "base", text_style: UITextStyleI = new UITextStyle) { + this.view = view; + this.game = view.gameui; + if (typeof parent == "string") { + this.parent = view.getLayer(parent); + } else { + this.parent = parent; + } + this.text_style = text_style; + } + + /** + * Create a new UIBuilder inside a parent container, or a view layer + * + * This new builder will inherit the style settings, and will create components in the specified parent + */ + in(container: UIBuilderParent | string, body?: (builder: UIBuilder) => void): UIBuilder { + let result = new UIBuilder(this.view, container, this.text_style); + if (body) { + body(result); + } + return result; + } + + /** + * Create a new UIBuilder with style changes + */ + styled(changes: UITextStyleI, body?: (builder: UIBuilder) => void): UIBuilder { + let result = new UIBuilder(this.view, this.parent, merge(this.text_style, changes)); + if (body) { + body(result); + } + return result; + } + + /** + * Clear the current container of all component + */ + clear(): void { + if (this.parent instanceof UIImage) { + console.error("Cannot clear an image parent, use groups instead"); + } else { + this.parent.removeAll(true); + } + } + + /** + * Internal method to add to the parent + */ + private add(child: UIText | UIImage | UIButton | UIContainer | UIGraphics): void { + if (this.parent instanceof UIImage) { + let gparent = this.parent.parentContainer; + if (gparent) { + let x = this.parent.x + child.x; + let y = this.parent.y + child.y; + child.setPosition(x, y); + gparent.add(child); + } else { + throw new Error("no parent container"); + } + } else { + this.parent.add(child); + } + } + + /** + * Add a container of other components + */ + container(name: string, x = 0, y = 0, visible = true): UIContainer { + let result = new UIContainer(this.view, x, y); + result.setName(name); + result.setVisible(visible); + this.add(result); + return result; + } + + /** + * Add a text + * + * Anchor will be defined according to the style centering + */ + text(content: string, x = 0, y = 0, style_changes: UITextStyleI = {}): UIText { + let style = merge(this.text_style, style_changes); + let result = new UIText(this.view, x, y, content, { + color: style.color, + align: style.center ? "center" : "left" + }); + result.setFont(`${style.bold ? "bold " : ""}${style.size}pt SpaceTac`); + result.setOrigin(style.center ? 0.5 : 0, style.vcenter ? 0.5 : 0); + if (style.width) { + result.setWordWrapWidth(style.width); + } + if (style.shadow) { + result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3, true, true); + } + if (style.stroke_width && style.stroke_color) { + result.setStroke(style.stroke_color, style.stroke_width); + } + this.add(result); + return result; + } + + /** + * Add an image + */ + image(name: string | string[], x = 0, y = 0, centered = false): UIImage { + if (typeof name != "string") { + name = this.view.getFirstImage(...name); + } + + let info = this.view.getImageInfo(name); + let result = new UIImage(this.view, x, y, info.key, info.frame); + result.name = name; + if (!centered) { + result.setOrigin(0); + } + this.add(result); + return result; + } + + /** + * Add a hoverable and/or clickable button + * + * If an image with "-hover" suffix is found in atlases, it will be used as hover mask (added as button child) + */ + button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}): UIButton { + options.text_style = merge(this.text_style, options.text_style || {}); + let result = new UIButton(this.view, name, x, y, onclick, tooltip, onoffcallback, options); + this.add(result); + return result; + } + + /** + * Add a value bar + */ + valuebar(name: string, x = 0, y = 0, orientation = ValueBarOrientation.EAST): ValueBar { + let result = new ValueBar(this.view, name, orientation, x, y); + this.add(result.node); + return result; + } + + /** + * Add a graphics (for drawing) + */ + graphics(name: string, x = 0, y = 0, visible = true): UIGraphics { + let result = new UIGraphics(this.view, name, visible, x, y); + this.add(result); + return result; + } + + /** + * Emit a bunch of particles + */ + particles(config: ParticlesConfig): void { + this.view.particles.emit(config, this.parent instanceof UIContainer ? this.parent : undefined); + } + + /** + * Animation to await something + */ + awaiter(x = 0, y = 0, visible = true, scale = 1): UIAwaiter { + let result = new UIAwaiter(this.view, x, y, visible); + result.setScale(scale); + this.add(result); + return result; + } + + /** + * Add a full-view capturing overlay + */ + overlay(options: UIOverlayOptions): UIOverlay { + let result = new UIOverlay(this.view, options); + this.add(result); + return result; + } + + /** + * Change the content of an component + * + * If the component is a text, its content will be changed. + * If the component is an image, its texture will be changed. + */ + change(component: UIImage | UIText, content: string): void { + // TODO Should be moved custom UIImage and UIText classes + if (component instanceof UIText) { + component.setText(content); + } else { + let info = this.view.getImageInfo(content); + component.setName(content); + component.setTexture(info.key, info.frame); + } + } + + /** + * Evenly distribute the children of this builder along an axis + */ + distribute(along: "x" | "y", start: number, end: number): void { + if (!(this.parent instanceof UIContainer)) { + throw new Error("UIBuilder.distribute only works on groups"); + } + let children = this.parent.list; + + let sizes = children.map(child => { + if (UITools.isSpatial(child)) { + return UITools.getBounds(child)[along == "x" ? "width" : "height"]; + } else { + return 0; + } + }); + let spacing = ((end - start) - sum(sizes)) / (sizes.length + 1); + let offset = start; + children.forEach((child, idx) => { + offset += spacing; + if (UITools.isSpatial(child)) { + child[along] = Math.round(offset); + } + offset += sizes[idx]; + }); + } } diff --git a/src/ui/common/UIButton.spec.ts b/src/ui/common/UIButton.spec.ts index 8ea1e75..b8fd273 100644 --- a/src/ui/common/UIButton.spec.ts +++ b/src/ui/common/UIButton.spec.ts @@ -1,120 +1,124 @@ -module TK.SpaceTac.UI.Specs { - testing("UIButton", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { identity, nop } from "../../common/Tools"; +import { setupEmptyView } from "../TestGame"; +import { UIBuilder } from "./UIBuilder"; +import { UIButtonUnicity } from "./UIButton"; - test.case("handles placement of masks", check => { - check.patch(testgame.view, "getImageInfo", (name: string) => ({ key: "test", frame: 0, exists: true })); - let builder = new UIBuilder(testgame.view); +testing("UIButton", test => { + let testgame = setupEmptyView(test); - check.in("both on top", check => { - let button = builder.button("button", 0, 0, nop, undefined, identity); - check.equals(button.length, 3); - builder.in(button).text("Test"); - check.equals(button.length, 4); - check.equals(button.list[0].name, "button"); - check.equals(button.list[1].type, "Text"); - check.equals(button.list[2].name, "button-on"); - check.equals(button.list[3].name, "button-hover"); - }); + test.case("handles placement of masks", check => { + check.patch(testgame.view, "getImageInfo", (name: string) => ({ key: "test", frame: 0, exists: true })); + let builder = new UIBuilder(testgame.view); - check.in("hover at bottom", check => { - let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true }); - check.equals(button.length, 3); - builder.in(button).text("Test"); - check.equals(button.length, 4); - check.equals(button.list[0].name, "button"); - check.equals(button.list[1].name, "button-hover"); - check.equals(button.list[2].type, "Text"); - check.equals(button.list[3].name, "button-on"); - }); - - check.in("'on' at bottom", check => { - let button = builder.button("button", 0, 0, nop, undefined, identity, { on_bottom: true }); - check.equals(button.length, 3); - builder.in(button).text("Test"); - check.equals(button.length, 4); - check.equals(button.list[0].name, "button"); - check.equals(button.list[1].name, "button-on"); - check.equals(button.list[2].type, "Text"); - check.equals(button.list[3].name, "button-hover"); - }); - - check.in("both at bottom", check => { - let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true, on_bottom: true }); - check.equals(button.length, 3); - builder.in(button).text("Test"); - check.equals(button.length, 4); - check.equals(button.list[0].name, "button"); - check.equals(button.list[1].name, "button-on"); - check.equals(button.list[2].name, "button-hover"); - check.equals(button.list[3].type, "Text"); - }); - }); - - test.case("toggles on/off", check => { - let builder = new UIBuilder(testgame.view); - let m1 = check.mockfunc("m1", (on: boolean) => on); - let button1 = builder.button("b1", 0, 0, undefined, undefined, m1.func); - let m2 = check.mockfunc("m1", (on: boolean) => on); - let button2 = builder.button("b2", 0, 0, undefined, undefined, m2.func); - let m3 = check.mockfunc("m1", (on: boolean) => on); - let button3 = builder.button("b3", 0, 0, undefined, undefined, m3.func); - - function verify(message: string, state1: boolean, state2: boolean, state3: boolean, called1: number, called2: number, called3: number) { - check.in(message, check => { - check.equals(button1.getState(), state1, "button1 state"); - check.equals(button2.getState(), state2, "button2 state"); - check.equals(button3.getState(), state3, "button3 state"); - check.called(m1, called1); - check.called(m2, called2); - check.called(m3, called3); - }); - } - - verify("initial", false, false, false, 0, 0, 0); - - button1.toggle(true); - verify("toggle on", true, false, false, 1, 0, 0); - - button1.toggle(true); - verify("toggle on again", true, false, false, 0, 0, 0); - - button1.toggle(false); - verify("toggle off", false, false, false, 1, 0, 0); - - button1.toggle(false); - verify("toggle off again", false, false, false, 0, 0, 0); - - button2.toggle(true, UIButtonUnicity.EXCLUSIVE); - verify("toggle on unicity - first", false, true, false, 0, 1, 0); - - button2.toggle(true, UIButtonUnicity.EXCLUSIVE); - verify("toggle on unicity - first again", false, true, false, 0, 0, 0); - - button3.toggle(true, UIButtonUnicity.EXCLUSIVE); - verify("toggle on unicity - second", false, false, true, 0, 1, 1); - - button2.toggle(false, UIButtonUnicity.EXCLUSIVE); - verify("toggle off unicity - other", false, false, true, 0, 0, 0); - - button3.toggle(false, UIButtonUnicity.EXCLUSIVE); - verify("toggle off unicity - currently on", false, false, false, 0, 0, 1); - - button1.toggle(true); - button2.toggle(true); - button3.toggle(true); - verify("toggle all on", true, true, true, 1, 1, 1); - - button2.toggle(true, UIButtonUnicity.EXCLUSIVE); - verify("toggle on unicity should shut down 2 others", false, true, false, 1, 0, 1); - - button2.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN); - verify("toggle off unicity min", false, true, false, 0, 0, 0); - - button1.toggle(); - verify("toggle auto on", true, true, false, 1, 0, 0); - button1.toggle(); - verify("toggle auto off", false, true, false, 1, 0, 0); - }); + check.in("both on top", check => { + let button = builder.button("button", 0, 0, nop, undefined, identity); + check.equals(button.length, 3); + builder.in(button).text("Test"); + check.equals(button.length, 4); + check.equals(button.list[0].name, "button"); + check.equals(button.list[1].type, "Text"); + check.equals(button.list[2].name, "button-on"); + check.equals(button.list[3].name, "button-hover"); }); -} + + check.in("hover at bottom", check => { + let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true }); + check.equals(button.length, 3); + builder.in(button).text("Test"); + check.equals(button.length, 4); + check.equals(button.list[0].name, "button"); + check.equals(button.list[1].name, "button-hover"); + check.equals(button.list[2].type, "Text"); + check.equals(button.list[3].name, "button-on"); + }); + + check.in("'on' at bottom", check => { + let button = builder.button("button", 0, 0, nop, undefined, identity, { on_bottom: true }); + check.equals(button.length, 3); + builder.in(button).text("Test"); + check.equals(button.length, 4); + check.equals(button.list[0].name, "button"); + check.equals(button.list[1].name, "button-on"); + check.equals(button.list[2].type, "Text"); + check.equals(button.list[3].name, "button-hover"); + }); + + check.in("both at bottom", check => { + let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true, on_bottom: true }); + check.equals(button.length, 3); + builder.in(button).text("Test"); + check.equals(button.length, 4); + check.equals(button.list[0].name, "button"); + check.equals(button.list[1].name, "button-on"); + check.equals(button.list[2].name, "button-hover"); + check.equals(button.list[3].type, "Text"); + }); + }); + + test.case("toggles on/off", check => { + let builder = new UIBuilder(testgame.view); + let m1 = check.mockfunc("m1", (on: boolean) => on); + let button1 = builder.button("b1", 0, 0, undefined, undefined, m1.func); + let m2 = check.mockfunc("m1", (on: boolean) => on); + let button2 = builder.button("b2", 0, 0, undefined, undefined, m2.func); + let m3 = check.mockfunc("m1", (on: boolean) => on); + let button3 = builder.button("b3", 0, 0, undefined, undefined, m3.func); + + function verify(message: string, state1: boolean, state2: boolean, state3: boolean, called1: number, called2: number, called3: number) { + check.in(message, check => { + check.equals(button1.getState(), state1, "button1 state"); + check.equals(button2.getState(), state2, "button2 state"); + check.equals(button3.getState(), state3, "button3 state"); + check.called(m1, called1); + check.called(m2, called2); + check.called(m3, called3); + }); + } + + verify("initial", false, false, false, 0, 0, 0); + + button1.toggle(true); + verify("toggle on", true, false, false, 1, 0, 0); + + button1.toggle(true); + verify("toggle on again", true, false, false, 0, 0, 0); + + button1.toggle(false); + verify("toggle off", false, false, false, 1, 0, 0); + + button1.toggle(false); + verify("toggle off again", false, false, false, 0, 0, 0); + + button2.toggle(true, UIButtonUnicity.EXCLUSIVE); + verify("toggle on unicity - first", false, true, false, 0, 1, 0); + + button2.toggle(true, UIButtonUnicity.EXCLUSIVE); + verify("toggle on unicity - first again", false, true, false, 0, 0, 0); + + button3.toggle(true, UIButtonUnicity.EXCLUSIVE); + verify("toggle on unicity - second", false, false, true, 0, 1, 1); + + button2.toggle(false, UIButtonUnicity.EXCLUSIVE); + verify("toggle off unicity - other", false, false, true, 0, 0, 0); + + button3.toggle(false, UIButtonUnicity.EXCLUSIVE); + verify("toggle off unicity - currently on", false, false, false, 0, 0, 1); + + button1.toggle(true); + button2.toggle(true); + button3.toggle(true); + verify("toggle all on", true, true, true, 1, 1, 1); + + button2.toggle(true, UIButtonUnicity.EXCLUSIVE); + verify("toggle on unicity should shut down 2 others", false, true, false, 1, 0, 1); + + button2.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN); + verify("toggle off unicity min", false, true, false, 0, 0, 0); + + button1.toggle(); + verify("toggle auto on", true, true, false, 1, 0, 0); + button1.toggle(); + verify("toggle auto off", false, true, false, 1, 0, 0); + }); +}); diff --git a/src/ui/common/UIButton.ts b/src/ui/common/UIButton.ts index ee9871b..35192ec 100644 --- a/src/ui/common/UIButton.ts +++ b/src/ui/common/UIButton.ts @@ -1,219 +1,223 @@ -/// +import { bool, first } from "../../common/Tools" +import { BaseView } from "../BaseView" +import { TooltipFiller } from "./Tooltip" +import { UIBuilder, UIOnOffCallback, UITextStyleI } from "./UIBuilder" +import { UIContainer } from "./UIContainer" +import { UIImage } from "./UIImage" +import { UIText } from "./UIText" -module TK.SpaceTac.UI { - /** - * Button options - */ - export type UIButtonOptions = { - // Centering - center?: boolean +/** + * Button options + */ +export type UIButtonOptions = { + // Centering + center?: boolean - // Name of the hover picture (by default, the button name, with "-hover" appended) - hover_name?: string + // Name of the hover picture (by default, the button name, with "-hover" appended) + hover_name?: string - // Name of the "on" picture (by default, the button name, with "-on" appended) - on_name?: string + // Name of the "on" picture (by default, the button name, with "-on" appended) + on_name?: string - // Whether "hover" picture should stay near the button (otherwise will be on top) - hover_bottom?: boolean + // Whether "hover" picture should stay near the button (otherwise will be on top) + hover_bottom?: boolean - // Whether "on" picture should stay near the button (otherwise will be on top) - on_bottom?: boolean + // Whether "on" picture should stay near the button (otherwise will be on top) + on_bottom?: boolean - // Text content - text?: string - text_x?: number - text_y?: number + // Text content + text?: string + text_x?: number + text_y?: number - // Text content style override - text_style?: UITextStyleI + // Text content style override + text_style?: UITextStyleI - // Icon content - icon?: string - icon_x?: number - icon_y?: number + // Icon content + icon?: string + icon_x?: number + icon_y?: number - // Unicity setting to control other buttons in the same container - unicity?: UIButtonUnicity - } - - /** - * When toggling a button status, this describes the behavior of other buttons in the same container - */ - export enum UIButtonUnicity { - // Do nothing to other buttons - NONE = 0, - // Shut down other buttons when one is toggled on - EXCLUSIVE = 1, - // Shut down other buttons when one is toggled on, but prevent to shut down the one currently on - EXCLUSIVE_MIN = 2 - } - - /** - * Button for UI, with support for hover, click, and on/off state - */ - export class UIButton extends UIContainer { - private base: UIImage - private state_on = false - readonly state_changer?: Function - private hover_mask?: UIImage - private hover_bottom = false - private on_mask?: UIImage - private on_bottom = false - private constructed = false - - constructor(private view: BaseView, key: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}) { - super(view, x, y); - this.setName(key); - - let builder = new UIBuilder(view, this, options.text_style); - let base = builder.image(key, 0, 0, options.center); - this.add(base); - this.base = base; - - let clickable = bool(onclick || onoffcallback); - let interactive = bool(clickable || tooltip); - - if (interactive) { - this.setInteractive({ - hitArea: new Phaser.Geom.Rectangle( - options.center ? 0 : base.width / 2, - options.center ? 0 : base.height / 2, - base.width, - base.height - ), - hitAreaCallback: Phaser.Geom.Rectangle.Contains, - cursor: "url(cursors/button.cur), pointer" - }); - - // On mask - if (onoffcallback) { - let on_name = options.on_name || (key + "-on"); - let on_info = view.getImageInfo(on_name); - if (on_info.exists) { - this.on_mask = builder.image(on_name, 0, 0, options.center); - this.on_mask.setVisible(false); - this.on_bottom = bool(options.on_bottom); - } - this.state_changer = (on: boolean): boolean => { - this.state_on = onoffcallback(on); - if (this.on_mask) { - view.animations.setVisible(this.on_mask, this.state_on, 100); - } - return this.state_on; - } - } - - // Hover mask - let hover_name = options.hover_name || (key + "-hover"); - let hover_info = view.getImageInfo(hover_name); - if (hover_info.exists) { - this.hover_mask = builder.image(hover_name, 0, 0, options.center); - this.hover_mask.setVisible(false); - this.hover_bottom = bool(options.hover_bottom); - if (this.hover_bottom && this.on_mask && !this.on_bottom) { - this.moveDown(this.hover_mask); - } - } - - view.inputs.setHoverClick(this, - () => { - if (tooltip) { - view.tooltip.show(this, tooltip); - } - if (this.hover_mask) { - view.animations.show(this.hover_mask, 100); - } - }, - () => { - if (tooltip) { - view.tooltip.hide(); - } - if (this.hover_mask) { - view.animations.hide(this.hover_mask, 100) - } - }, - () => { - if (clickable && onclick) { - onclick(); - } else if (onoffcallback) { - this.toggle(!this.state_on, options.unicity); - } - }, 100, undefined, clickable); - } - - if (options.text) { - builder.text(options.text, options.text_x || 0, options.text_y || 0, options.text_style); - } - - if (options.icon) { - builder.image(options.icon, options.icon_x || 0, options.icon_y || 0, options.center); - } - - this.constructed = true; - } - - add(child: UIImage | UIText): UIButton { - if (this.constructed) { - // Protect the "on" and "hover" layers - let layer = first(this.list, child => (!this.hover_bottom && child == this.hover_mask) || (!this.on_bottom && child == this.on_mask)); - if (layer) { - super.addAt(child, this.getIndex(layer)); - } else { - super.add(child); - } - } else { - super.add(child); - } - return this; - } - - get width(): number { - return this.base.width; - } - - get height(): number { - return this.base.height; - } - - /** - * Get the state on/off - */ - getState(): boolean { - return this.state_on; - } - - /** - * Change the base texture - */ - setBaseImage(key: string): void { - this.view.changeImage(this.base, key); - this.setName(key); - } - - /** - * Select this button status - * - * Returns the final state of this button - */ - toggle(on?: boolean, unicity?: UIButtonUnicity): boolean { - if (typeof on == "undefined") { - on = !this.state_on; - } - - if (on && unicity && this.parentContainer) { - this.parentContainer.list.forEach(child => { - if (child instanceof UIButton && child != this) { - child.toggle(false); - } - }); - } - - if (this.state_changer && (on || unicity != UIButtonUnicity.EXCLUSIVE_MIN) && on != this.state_on) { - this.state_changer(on); - } - - return this.state_on; - } - } + // Unicity setting to control other buttons in the same container + unicity?: UIButtonUnicity +} + +/** + * When toggling a button status, this describes the behavior of other buttons in the same container + */ +export enum UIButtonUnicity { + // Do nothing to other buttons + NONE = 0, + // Shut down other buttons when one is toggled on + EXCLUSIVE = 1, + // Shut down other buttons when one is toggled on, but prevent to shut down the one currently on + EXCLUSIVE_MIN = 2 +} + +/** + * Button for UI, with support for hover, click, and on/off state + */ +export class UIButton extends UIContainer { + private base: UIImage + private state_on = false + readonly state_changer?: Function + private hover_mask?: UIImage + private hover_bottom = false + private on_mask?: UIImage + private on_bottom = false + private constructed = false + + constructor(private view: BaseView, key: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}) { + super(view, x, y); + this.setName(key); + + let builder = new UIBuilder(view, this, options.text_style); + let base = builder.image(key, 0, 0, options.center); + this.add(base); + this.base = base; + + let clickable = bool(onclick || onoffcallback); + let interactive = bool(clickable || tooltip); + + if (interactive) { + this.setInteractive({ + hitArea: new Phaser.Geom.Rectangle( + options.center ? 0 : base.width / 2, + options.center ? 0 : base.height / 2, + base.width, + base.height + ), + hitAreaCallback: Phaser.Geom.Rectangle.Contains, + cursor: "url(cursors/button.cur), pointer" + }); + + // On mask + if (onoffcallback) { + let on_name = options.on_name || (key + "-on"); + let on_info = view.getImageInfo(on_name); + if (on_info.exists) { + this.on_mask = builder.image(on_name, 0, 0, options.center); + this.on_mask.setVisible(false); + this.on_bottom = bool(options.on_bottom); + } + this.state_changer = (on: boolean): boolean => { + this.state_on = onoffcallback(on); + if (this.on_mask) { + view.animations.setVisible(this.on_mask, this.state_on, 100); + } + return this.state_on; + } + } + + // Hover mask + let hover_name = options.hover_name || (key + "-hover"); + let hover_info = view.getImageInfo(hover_name); + if (hover_info.exists) { + this.hover_mask = builder.image(hover_name, 0, 0, options.center); + this.hover_mask.setVisible(false); + this.hover_bottom = bool(options.hover_bottom); + if (this.hover_bottom && this.on_mask && !this.on_bottom) { + this.moveDown(this.hover_mask); + } + } + + view.inputs.setHoverClick(this, + () => { + if (tooltip) { + view.tooltip.show(this, tooltip); + } + if (this.hover_mask) { + view.animations.show(this.hover_mask, 100); + } + }, + () => { + if (tooltip) { + view.tooltip.hide(); + } + if (this.hover_mask) { + view.animations.hide(this.hover_mask, 100) + } + }, + () => { + if (clickable && onclick) { + onclick(); + } else if (onoffcallback) { + this.toggle(!this.state_on, options.unicity); + } + }, 100, undefined, clickable); + } + + if (options.text) { + builder.text(options.text, options.text_x || 0, options.text_y || 0, options.text_style); + } + + if (options.icon) { + builder.image(options.icon, options.icon_x || 0, options.icon_y || 0, options.center); + } + + this.constructed = true; + } + + add(child: UIImage | UIText): UIButton { + if (this.constructed) { + // Protect the "on" and "hover" layers + let layer = first(this.list, child => (!this.hover_bottom && child == this.hover_mask) || (!this.on_bottom && child == this.on_mask)); + if (layer) { + super.addAt(child, this.getIndex(layer)); + } else { + super.add(child); + } + } else { + super.add(child); + } + return this; + } + + get width(): number { + return this.base.width; + } + + get height(): number { + return this.base.height; + } + + /** + * Get the state on/off + */ + getState(): boolean { + return this.state_on; + } + + /** + * Change the base texture + */ + setBaseImage(key: string): void { + this.view.changeImage(this.base, key); + this.setName(key); + } + + /** + * Select this button status + * + * Returns the final state of this button + */ + toggle(on?: boolean, unicity?: UIButtonUnicity): boolean { + if (typeof on == "undefined") { + on = !this.state_on; + } + + if (on && unicity && this.parentContainer) { + this.parentContainer.list.forEach(child => { + if (child instanceof UIButton && child != this) { + child.toggle(false); + } + }); + } + + if (this.state_changer && (on || unicity != UIButtonUnicity.EXCLUSIVE_MIN) && on != this.state_on) { + this.state_changer(on); + } + + return this.state_on; + } } diff --git a/src/ui/common/UIConfirmDialog.ts b/src/ui/common/UIConfirmDialog.ts index bfe40e6..00e4a10 100644 --- a/src/ui/common/UIConfirmDialog.ts +++ b/src/ui/common/UIConfirmDialog.ts @@ -1,46 +1,47 @@ -module TK.SpaceTac.UI { - /** - * Dialog asking for a confirmation - */ - export class UIConfirmDialog extends UIDialog { - private result: Promise - private result_resolver?: (confirmed: boolean) => void +import { BaseView } from "../BaseView"; +import { UIDialog } from "./UIDialog"; - constructor(view: BaseView, message: string) { - super(view); +/** + * Dialog asking for a confirmation + */ +export class UIConfirmDialog extends UIDialog { + private result: Promise + private result_resolver?: (confirmed: boolean) => void - this.content.text(message, this.width * 0.5, this.height * 0.4, { color: "#9FC4D6", size: 32, shadow: true }); + constructor(view: BaseView, message: string) { + super(view); - this.result = new Promise(resolve => { - this.result_resolver = resolve; + this.content.text(message, this.width * 0.5, this.height * 0.4, { color: "#9FC4D6", size: 32, shadow: true }); - this.content.button("menu-button-small", this.width * 0.4, this.height * 0.6, () => resolve(false), - undefined, undefined, { center: true, text: "Cancel", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); - this.content.button("menu-button-small", this.width * 0.6, this.height * 0.6, () => resolve(true), - undefined, undefined, { center: true, text: "OK", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); - }); - } + this.result = new Promise(resolve => { + this.result_resolver = resolve; - /** - * Force the result (simulate clicking the appropriate button) - */ - async forceResult(confirmed: boolean): Promise { - if (this.result_resolver) { - this.result_resolver(confirmed); - await this.result; - } - } + this.content.button("menu-button-small", this.width * 0.4, this.height * 0.6, () => resolve(false), + undefined, undefined, { center: true, text: "Cancel", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); + this.content.button("menu-button-small", this.width * 0.6, this.height * 0.6, () => resolve(true), + undefined, undefined, { center: true, text: "OK", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); + }); + } - /** - * Convenient function to ask for a confirmation, and have a promise of result - */ - static ask(view: BaseView, message: string): Promise { - let dlg = new UIConfirmDialog(view, message); - let result = dlg.result; - return result.then(confirmed => { - dlg.close(); - return confirmed; - }); - } + /** + * Force the result (simulate clicking the appropriate button) + */ + async forceResult(confirmed: boolean): Promise { + if (this.result_resolver) { + this.result_resolver(confirmed); + await this.result; } -} \ No newline at end of file + } + + /** + * Convenient function to ask for a confirmation, and have a promise of result + */ + static ask(view: BaseView, message: string): Promise { + let dlg = new UIConfirmDialog(view, message); + let result = dlg.result; + return result.then(confirmed => { + dlg.close(); + return confirmed; + }); + } +} diff --git a/src/ui/common/UIContainer.ts b/src/ui/common/UIContainer.ts index c8ece1b..aede45c 100644 --- a/src/ui/common/UIContainer.ts +++ b/src/ui/common/UIContainer.ts @@ -1,50 +1,52 @@ -module TK.SpaceTac.UI { - /** - * UI component able to contain other UI components - */ - export class UIContainer extends Phaser.GameObjects.Container { - /** - * Get a container to build UI components inside the container - */ - getBuilder(): UIBuilder { - return new UIBuilder(this.scene, this); - } - - /** - * Fixed version that does not force (0, 0) to be in bounds - */ - getBounds(output?: Phaser.Geom.Rectangle): Phaser.Geom.Rectangle { - let result: IBounded = { x: 0, y: 0, width: 0, height: 0 }; - - if (this.list.length > 0) { - var children = this.list; - - for (var i = 0; i < children.length; i++) { - var entry = children[i]; - - if (UITools.isSpatial(entry)) { - result = UITools.unionRects(result, entry.getBounds()); - } - } - } - - if (typeof output == "undefined") { - output = new Phaser.Geom.Rectangle(); - } - output.setTo(result.x, result.y, result.width, result.height); - return output; - } - - /** - * Overload of setVisible, with fading support - */ - setVisible(visible: boolean, duration = 0): this { - if (duration) { - (this.scene).animations.setVisible(this, visible, duration); - } else { - super.setVisible(visible); - } - return this; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "./UIBuilder"; +import { IBounded, UITools } from "./UITools"; + +/** + * UI component able to contain other UI components + */ +export class UIContainer extends Phaser.GameObjects.Container { + /** + * Get a container to build UI components inside the container + */ + getBuilder(): UIBuilder { + return new UIBuilder(this.scene, this); + } + + /** + * Fixed version that does not force (0, 0) to be in bounds + */ + getBounds(output?: Phaser.Geom.Rectangle): Phaser.Geom.Rectangle { + let result: IBounded = { x: 0, y: 0, width: 0, height: 0 }; + + if (this.list.length > 0) { + var children = this.list; + + for (var i = 0; i < children.length; i++) { + var entry = children[i]; + + if (UITools.isSpatial(entry)) { + result = UITools.unionRects(result, entry.getBounds()); } + } } + + if (typeof output == "undefined") { + output = new Phaser.Geom.Rectangle(); + } + output.setTo(result.x, result.y, result.width, result.height); + return output; + } + + /** + * Overload of setVisible, with fading support + */ + setVisible(visible: boolean, duration = 0): this { + if (duration) { + (this.scene).animations.setVisible(this, visible, duration); + } else { + super.setVisible(visible); + } + return this; + } } diff --git a/src/ui/common/UIConversation.ts b/src/ui/common/UIConversation.ts index e543158..1b58f7f 100644 --- a/src/ui/common/UIConversation.ts +++ b/src/ui/common/UIConversation.ts @@ -1,233 +1,237 @@ -module TK.SpaceTac.UI { - export type UIConversationPiece = { interlocutor: Ship, message: string } - export type UIConversationCallback = (conversation: UIConversation, step: number) => boolean +import { Ship } from "../../core/Ship" +import { BaseView } from "../BaseView" +import { UIBuilder, UITextStyleI } from "./UIBuilder" +import { UIContainer } from "./UIContainer" +import { UIOverlay } from "./UIOverlay" - /** - * Style for a conversational message display - */ - export class UIConversationStyle { - // Center the message or not - center = false +export type UIConversationPiece = { interlocutor: Ship, message: string } +export type UIConversationCallback = (conversation: UIConversation, step: number) => boolean - // Padding between the content and the external border - padding = 10 +/** + * Style for a conversational message display + */ +export class UIConversationStyle { + // Center the message or not + center = false - // Background fill color - background = 0x1B3B4B - alpha = 0.9 + // Padding between the content and the external border + padding = 10 - // Border color and width - border = 0x3A6479 - border_width = 2 + // Background fill color + background = 0x1B3B4B + alpha = 0.9 - // Text style - text: UITextStyleI = { - color: "#DBEFF9", - size: 20, - bold: true, - shadow: true + // Border color and width + border = 0x3A6479 + border_width = 2 + + // Text style + text: UITextStyleI = { + color: "#DBEFF9", + size: 20, + bold: true, + shadow: true + } + + // Portrait or image to display (from atlases) + image = "" + image_size = 0 + image_caption = "" +} + +/** + * Rectangle to display a message that may appear progressively, as in conversations + */ +export class UIConversationMessage { + private container: UIContainer + + constructor(private builder: UIBuilder, private width: number, private height: number, message: string, style = new UIConversationStyle(), forward?: Function) { + this.container = builder.container("conversation-message"); + + builder.styled(style.text).in(this.container, builder => { + if (!style.center) { + builder = builder.styled({ center: false, vcenter: false }); + } + + builder.graphics("background").addRectangle({ x: 0, y: 0, width: width, height: height }, + style.background, style.border_width, style.border, style.alpha); + + let offset = 0; + if (style.image_size && style.image) { + offset = style.image_size + style.padding; + width -= offset; + + let ioffset = style.padding + Math.floor(style.image_size / 2); + builder.image(style.image, ioffset, ioffset, true); + + if (style.image_caption) { + let text_size = Math.ceil(style.text.size ? style.text.size * 0.6 : 16); + builder.text(style.image_caption, ioffset, style.padding + style.image_size + text_size, { + size: text_size, + center: true + }); } + } - // Portrait or image to display (from atlases) - image = "" - image_size = 0 - image_caption = "" + let text = builder.text(message, offset + (style.center ? width / 2 : style.padding), style.center ? height / 2 : style.padding, { + width: width - style.padding * 2 + }); + + /*let i = 0; + let colorchar = () => { + text.clearColors(); + if (i < message.length) { + text.addColor("transparent", i); + i++; + this.view.timer.schedule(10, colorchar); + } + } + colorchar();*/ + + if (forward) { + builder.button("common-arrow", this.width - 30, this.height - 30, forward, "Next", undefined, { center: true }); + } + }); + } + + destroy() { + this.container.destroy(); + } + + positionRelative(relx: number, rely: number) { + let view = this.builder.view; + let rx = (view.getWidth() - this.width) * relx; + let ry = (view.getHeight() - this.height) * rely; + this.container.setPosition(Math.round(rx), Math.round(ry)); + } + + setVisible(visible: boolean, duration = 0): void { + this.container.setVisible(visible, duration); + } +} + +/** + * Display of an active conversation (sequence of messages) + */ +export class UIConversation { + private view: BaseView + private builder: UIBuilder + private container: UIContainer + private overlay: UIOverlay + private message?: UIConversationMessage + private step = -1 + private on_step: UIConversationCallback + private ended = false + private on_end = new Phaser.Events.EventEmitter() + + constructor(builder: UIBuilder, on_step: UIConversationCallback) { + this.view = builder.view; + + this.container = builder.container("conversation"); + this.builder = builder.in(this.container); + this.overlay = this.builder.overlay({ + color: 0x404450, + alpha: 0.7, + on_click: () => this.forward() + }); + this.setVisible(false); + + this.on_step = on_step; + + this.forward(); + } + + /** + * Clear the content of previous message, if any + */ + clearContent(): void { + if (this.message) { + this.message.destroy(); + this.message = undefined; + } + } + + /** + * Set the global visibility + */ + setVisible(visible: boolean, duration = 0): void { + this.container.setVisible(visible, duration); + } + + /** + * Destroy the conversation handler + */ + destroy() { + if (!this.ended) { + this.ended = true; + this.on_end.emit("done"); } - /** - * Rectangle to display a message that may appear progressively, as in conversations - */ - export class UIConversationMessage { - private container: UIContainer + this.container.destroy(); + } - constructor(private builder: UIBuilder, private width: number, private height: number, message: string, style = new UIConversationStyle(), forward?: Function) { - this.container = builder.container("conversation-message"); - - builder.styled(style.text).in(this.container, builder => { - if (!style.center) { - builder = builder.styled({ center: false, vcenter: false }); - } - - builder.graphics("background").addRectangle({ x: 0, y: 0, width: width, height: height }, - style.background, style.border_width, style.border, style.alpha); - - let offset = 0; - if (style.image_size && style.image) { - offset = style.image_size + style.padding; - width -= offset; - - let ioffset = style.padding + Math.floor(style.image_size / 2); - builder.image(style.image, ioffset, ioffset, true); - - if (style.image_caption) { - let text_size = Math.ceil(style.text.size ? style.text.size * 0.6 : 16); - builder.text(style.image_caption, ioffset, style.padding + style.image_size + text_size, { - size: text_size, - center: true - }); - } - } - - let text = builder.text(message, offset + (style.center ? width / 2 : style.padding), style.center ? height / 2 : style.padding, { - width: width - style.padding * 2 - }); - - /*let i = 0; - let colorchar = () => { - text.clearColors(); - if (i < message.length) { - text.addColor("transparent", i); - i++; - this.view.timer.schedule(10, colorchar); - } - } - colorchar();*/ - - if (forward) { - builder.button("common-arrow", this.width - 30, this.height - 30, forward, "Next", undefined, { center: true }); - } - }); - } - - destroy() { - this.container.destroy(); - } - - positionRelative(relx: number, rely: number) { - let view = this.builder.view; - let rx = (view.getWidth() - this.width) * relx; - let ry = (view.getHeight() - this.height) * rely; - this.container.setPosition(Math.round(rx), Math.round(ry)); - } - - setVisible(visible: boolean, duration = 0): void { - this.container.setVisible(visible, duration); - } + /** + * Promise to wait for the end of conversation + */ + waitEnd(): Promise { + if (this.ended) { + return Promise.resolve(); + } else { + return new Promise(resolve => { + this.on_end.on("done", resolve); + }); } + } - /** - * Display of an active conversation (sequence of messages) - */ - export class UIConversation { - private view: BaseView - private builder: UIBuilder - private container: UIContainer - private overlay: UIOverlay - private message?: UIConversationMessage - private step = -1 - private on_step: UIConversationCallback - private ended = false - private on_end = new Phaser.Events.EventEmitter() + /** + * Set the currently displayed message + */ + setCurrentMessage(style: UIConversationStyle, content: string, width: number, height: number, relx: number, rely: number): void { + this.clearContent(); - constructor(builder: UIBuilder, on_step: UIConversationCallback) { - this.view = builder.view; + this.message = new UIConversationMessage(this.builder, width, height, content, style, () => this.forward()); + this.message.positionRelative(relx, rely); - this.container = builder.container("conversation"); - this.builder = builder.in(this.container); - this.overlay = this.builder.overlay({ - color: 0x404450, - alpha: 0.7, - on_click: () => this.forward() - }); - this.setVisible(false); + this.setVisible(true, 700); + } - this.on_step = on_step; + /** + * Convenience to set the current message from a ship + * + * This will automatically set the style and position of the message + */ + setCurrentShipMessage(ship: Ship, content: string): void { + let style = new UIConversationStyle(); + style.image = `ship-${ship.model.code}-portrait`; + style.image_caption = ship.getName(false); + style.image_size = 256; - this.forward(); - } + let own = this.view.gameui.session.player.is(ship.fleet.player); + this.setCurrentMessage(style, content, 900, 310, own ? 0.1 : 0.9, own ? 0.2 : 0.8); + } - /** - * Clear the content of previous message, if any - */ - clearContent(): void { - if (this.message) { - this.message.destroy(); - this.message = undefined; - } - } - - /** - * Set the global visibility - */ - setVisible(visible: boolean, duration = 0): void { - this.container.setVisible(visible, duration); - } - - /** - * Destroy the conversation handler - */ - destroy() { - if (!this.ended) { - this.ended = true; - this.on_end.emit("done"); - } - - this.container.destroy(); - } - - /** - * Promise to wait for the end of conversation - */ - waitEnd(): Promise { - if (this.ended) { - return Promise.resolve(); - } else { - return new Promise(resolve => { - this.on_end.on("done", resolve); - }); - } - } - - /** - * Set the currently displayed message - */ - setCurrentMessage(style: UIConversationStyle, content: string, width: number, height: number, relx: number, rely: number): void { - this.clearContent(); - - this.message = new UIConversationMessage(this.builder, width, height, content, style, () => this.forward()); - this.message.positionRelative(relx, rely); - - this.setVisible(true, 700); - } - - /** - * Convenience to set the current message from a ship - * - * This will automatically set the style and position of the message - */ - setCurrentShipMessage(ship: Ship, content: string): void { - let style = new UIConversationStyle(); - style.image = `ship-${ship.model.code}-portrait`; - style.image_caption = ship.getName(false); - style.image_size = 256; - - let own = this.view.gameui.session.player.is(ship.fleet.player); - this.setCurrentMessage(style, content, 900, 310, own ? 0.1 : 0.9, own ? 0.2 : 0.8); - } - - /** - * Go forward to the next message - */ - forward(): void { - this.step += 1; - if (!this.on_step(this, this.step)) { - this.destroy(); - } - } - - /** - * Convenience to create a conversation from a list of pieces - */ - static newFromPieces(builder: UIBuilder, pieces: UIConversationPiece[]): UIConversation { - let result = new UIConversation(builder, (conversation, step) => { - if (step >= pieces.length) { - return false; - } else { - conversation.setCurrentShipMessage(pieces[step].interlocutor, pieces[step].message); - return true; - } - }); - return result; - } + /** + * Go forward to the next message + */ + forward(): void { + this.step += 1; + if (!this.on_step(this, this.step)) { + this.destroy(); } -} \ No newline at end of file + } + + /** + * Convenience to create a conversation from a list of pieces + */ + static newFromPieces(builder: UIBuilder, pieces: UIConversationPiece[]): UIConversation { + let result = new UIConversation(builder, (conversation, step) => { + if (step >= pieces.length) { + return false; + } else { + conversation.setCurrentShipMessage(pieces[step].interlocutor, pieces[step].message); + return true; + } + }); + return result; + } +} diff --git a/src/ui/common/UIDialog.spec.ts b/src/ui/common/UIDialog.spec.ts index b49f325..0d3d4db 100644 --- a/src/ui/common/UIDialog.spec.ts +++ b/src/ui/common/UIDialog.spec.ts @@ -1,39 +1,42 @@ -module TK.SpaceTac.UI.Specs { - testing("UIDialog", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { UIDialog } from "./UIDialog"; +import { UIOverlay } from "./UIOverlay"; - test.case("sets up an overlay", check => { - let view = testgame.view; - check.equals(view.dialogs_layer.length, 0, "initial"); +testing("UIDialog", test => { + let testgame = setupEmptyView(test); - let dialog1 = new UIDialog(view, "fake"); - check.in("one dialog", check => { - check.equals(view.dialogs_layer.length, 2); - check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); - check.same(view.dialogs_layer.list[1], dialog1.base); - }); + test.case("sets up an overlay", check => { + let view = testgame.view; + check.equals(view.dialogs_layer.length, 0, "initial"); - let dialog2 = new UIDialog(view, "fake"); - check.in("two dialogs", check => { - check.equals(view.dialogs_layer.length, 3); - check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); - check.same(view.dialogs_layer.list[1], dialog1.base); - check.same(view.dialogs_layer.list[2], dialog2.base); - }); - - dialog1.close(); - - check.in("one dialog closed", check => { - check.equals(view.dialogs_layer.length, 2); - check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); - check.same(view.dialogs_layer.list[1], dialog2.base); - }); - - dialog2.close(); - - check.in("all dialogs closed", check => { - check.equals(view.dialogs_layer.length, 0); - }); - }); + let dialog1 = new UIDialog(view, "fake"); + check.in("one dialog", check => { + check.equals(view.dialogs_layer.length, 2); + check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); + check.same(view.dialogs_layer.list[1], dialog1.base); }); -} + + let dialog2 = new UIDialog(view, "fake"); + check.in("two dialogs", check => { + check.equals(view.dialogs_layer.length, 3); + check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); + check.same(view.dialogs_layer.list[1], dialog1.base); + check.same(view.dialogs_layer.list[2], dialog2.base); + }); + + dialog1.close(); + + check.in("one dialog closed", check => { + check.equals(view.dialogs_layer.length, 2); + check.equals(view.dialogs_layer.list[0] instanceof UIOverlay, true); + check.same(view.dialogs_layer.list[1], dialog2.base); + }); + + dialog2.close(); + + check.in("all dialogs closed", check => { + check.equals(view.dialogs_layer.length, 0); + }); + }); +}); diff --git a/src/ui/common/UIDialog.ts b/src/ui/common/UIDialog.ts index b7f14bf..0a29f77 100644 --- a/src/ui/common/UIDialog.ts +++ b/src/ui/common/UIDialog.ts @@ -1,68 +1,71 @@ -module TK.SpaceTac.UI { - /** - * Base class for modal dialogs - * - * When a modal dialog opens, an overlay is displayed behind it to prevent clicking through it - */ - export class UIDialog { - readonly base: UIContainer - readonly content: UIBuilder - readonly width: number - readonly height: number +import { add, remove } from "../../common/Tools"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "./UIBuilder"; +import { UIContainer } from "./UIContainer"; - constructor(readonly view: BaseView, background_key = "common-dialog") { - if (view.dialogs_opened.length == 0) { - this.addOverlay(view.dialogs_layer); - } +/** + * Base class for modal dialogs + * + * When a modal dialog opens, an overlay is displayed behind it to prevent clicking through it + */ +export class UIDialog { + readonly base: UIContainer + readonly content: UIBuilder + readonly width: number + readonly height: number - let builder = new UIBuilder(view, view.dialogs_layer); - this.base = builder.container("dialog-base"); - builder = builder.in(this.base); - - let background = builder.image(background_key); - this.width = background.width; - this.height = background.height; - - this.base.setPosition((this.view.getWidth() - this.width) / 2, (this.view.getHeight() - this.height) / 2); - - this.content = builder.in(builder.container("content")); - - add(view.dialogs_opened, this); - - view.audio.playOnce("ui-dialog-open"); - } - - /** - * Add an input-capturing overlay - */ - addOverlay(layer: UIContainer): void { - new UIBuilder(this.view, layer).overlay({ - color: 0x888888, - alpha: 0.3 - }); - } - - /** - * Add a close button - */ - addCloseButton(key = "common-dialog-close", x = 1290, y = 90): void { - let builder = new UIBuilder(this.view, this.base); - builder.button(key, x, y, () => this.close(), "Close this dialog"); - } - - /** - * Close the dialog, removing the overlay if needed - */ - close() { - this.base.destroy(); - - this.view.audio.playOnce("ui-dialog-close"); - - remove(this.view.dialogs_opened, this); - if (this.view.dialogs_opened.length == 0) { - // Remove overlay - this.view.dialogs_layer.removeAll(true); - } - } + constructor(readonly view: BaseView, background_key = "common-dialog") { + if (view.dialogs_opened.length == 0) { + this.addOverlay(view.dialogs_layer); } + + let builder = new UIBuilder(view, view.dialogs_layer); + this.base = builder.container("dialog-base"); + builder = builder.in(this.base); + + let background = builder.image(background_key); + this.width = background.width; + this.height = background.height; + + this.base.setPosition((this.view.getWidth() - this.width) / 2, (this.view.getHeight() - this.height) / 2); + + this.content = builder.in(builder.container("content")); + + add(view.dialogs_opened, this); + + view.audio.playOnce("ui-dialog-open"); + } + + /** + * Add an input-capturing overlay + */ + addOverlay(layer: UIContainer): void { + new UIBuilder(this.view, layer).overlay({ + color: 0x888888, + alpha: 0.3 + }); + } + + /** + * Add a close button + */ + addCloseButton(key = "common-dialog-close", x = 1290, y = 90): void { + let builder = new UIBuilder(this.view, this.base); + builder.button(key, x, y, () => this.close(), "Close this dialog"); + } + + /** + * Close the dialog, removing the overlay if needed + */ + close() { + this.base.destroy(); + + this.view.audio.playOnce("ui-dialog-close"); + + remove(this.view.dialogs_opened, this); + if (this.view.dialogs_opened.length == 0) { + // Remove overlay + this.view.dialogs_layer.removeAll(true); + } + } } diff --git a/src/ui/common/UIGraphics.ts b/src/ui/common/UIGraphics.ts index 259533f..bab1156 100644 --- a/src/ui/common/UIGraphics.ts +++ b/src/ui/common/UIGraphics.ts @@ -1,96 +1,98 @@ -module TK.SpaceTac.UI { - export interface UIGraphicsCircleOptions { - center?: { x: number, y: number } - radius: number - fill?: { color: number, alpha?: number } - border?: { color: number, width?: number, alpha?: number } - } +import { coalesce } from "../../common/Tools"; +import { BaseView } from "../BaseView"; +import { IBounded } from "./UITools"; - export interface UIGraphicsCirclePortionOptions extends UIGraphicsCircleOptions { - angle: { start: number, span: number } - } - - export interface UIGraphicsLineOptions { - start: { x: number, y: number } - end: { x: number, y: number } - color: number, - alpha?: number, - width?: number - } - - /** - * UI component that supports drawing simple shapes (circles, lines...) - */ - export class UIGraphics extends Phaser.GameObjects.Graphics { - constructor(view: BaseView, name: string, visible = true, x = 0, y = 0) { - super(view); - this.setName(name); - this.setVisible(visible); - this.setPosition(x, y); - } - - /** - * Add a rectangle - */ - addRectangle(shape: IBounded, color: number, border_width = 0, border_color?: number, alpha = 1): void { - let rect = new Phaser.Geom.Rectangle(shape.x, shape.y, shape.width, shape.height); - - this.fillStyle(color, alpha); - this.fillRectShape(rect); - if (border_width && border_color) { - this.lineStyle(border_width, border_color, alpha); - this.strokeRectShape(rect); - } - } - - /** - * Add a portion of circle - */ - addCircleArc(options: UIGraphicsCirclePortionOptions): void { - let x = options.center ? options.center.x : 0; - let y = options.center ? options.center.y : 0; - - if (options.fill) { - this.fillStyle(options.fill.color, options.fill.alpha); - this.slice(x, y, options.radius, options.angle.start, options.angle.start + options.angle.span, false); - this.fillPath(); - } - - if (options.border) { - this.lineStyle(options.border.width || 1, options.border.color, options.border.alpha); - this.slice(x, y, options.radius, options.angle.start, options.angle.start + options.angle.span, false); - this.strokePath(); - } - } - - /** - * Add a full circle - */ - addCircle(options: UIGraphicsCircleOptions): void { - let x = options.center ? options.center.x : 0; - let y = options.center ? options.center.y : 0; - - if (options.fill) { - this.fillStyle(options.fill.color, options.fill.alpha); - this.fillCircle(x, y, options.radius); - } - - if (options.border) { - this.lineStyle(options.border.width || 1, options.border.color, options.border.alpha); - this.strokeCircle(x, y, options.radius); - } - } - - /** - * Add a line - */ - addLine(options: UIGraphicsLineOptions): void { - this.beginPath(); - this.lineStyle(coalesce(options.width, 1), options.color, coalesce(options.alpha, 1)); - this.moveTo(options.start.x, options.start.y); - this.lineTo(options.end.x, options.end.y); - this.closePath(); - this.strokePath(); - } - } +export interface UIGraphicsCircleOptions { + center?: { x: number, y: number } + radius: number + fill?: { color: number, alpha?: number } + border?: { color: number, width?: number, alpha?: number } +} + +export interface UIGraphicsCirclePortionOptions extends UIGraphicsCircleOptions { + angle: { start: number, span: number } +} + +export interface UIGraphicsLineOptions { + start: { x: number, y: number } + end: { x: number, y: number } + color: number, + alpha?: number, + width?: number +} + +/** + * UI component that supports drawing simple shapes (circles, lines...) + */ +export class UIGraphics extends Phaser.GameObjects.Graphics { + constructor(view: BaseView, name: string, visible = true, x = 0, y = 0) { + super(view); + this.setName(name); + this.setVisible(visible); + this.setPosition(x, y); + } + + /** + * Add a rectangle + */ + addRectangle(shape: IBounded, color: number, border_width = 0, border_color?: number, alpha = 1): void { + let rect = new Phaser.Geom.Rectangle(shape.x, shape.y, shape.width, shape.height); + + this.fillStyle(color, alpha); + this.fillRectShape(rect); + if (border_width && border_color) { + this.lineStyle(border_width, border_color, alpha); + this.strokeRectShape(rect); + } + } + + /** + * Add a portion of circle + */ + addCircleArc(options: UIGraphicsCirclePortionOptions): void { + let x = options.center ? options.center.x : 0; + let y = options.center ? options.center.y : 0; + + if (options.fill) { + this.fillStyle(options.fill.color, options.fill.alpha); + this.slice(x, y, options.radius, options.angle.start, options.angle.start + options.angle.span, false); + this.fillPath(); + } + + if (options.border) { + this.lineStyle(options.border.width || 1, options.border.color, options.border.alpha); + this.slice(x, y, options.radius, options.angle.start, options.angle.start + options.angle.span, false); + this.strokePath(); + } + } + + /** + * Add a full circle + */ + addCircle(options: UIGraphicsCircleOptions): void { + let x = options.center ? options.center.x : 0; + let y = options.center ? options.center.y : 0; + + if (options.fill) { + this.fillStyle(options.fill.color, options.fill.alpha); + this.fillCircle(x, y, options.radius); + } + + if (options.border) { + this.lineStyle(options.border.width || 1, options.border.color, options.border.alpha); + this.strokeCircle(x, y, options.radius); + } + } + + /** + * Add a line + */ + addLine(options: UIGraphicsLineOptions): void { + this.beginPath(); + this.lineStyle(coalesce(options.width, 1), options.color, coalesce(options.alpha, 1)); + this.moveTo(options.start.x, options.start.y); + this.lineTo(options.end.x, options.end.y); + this.closePath(); + this.strokePath(); + } } diff --git a/src/ui/common/UIImage.ts b/src/ui/common/UIImage.ts index 5a4887a..57c0678 100644 --- a/src/ui/common/UIImage.ts +++ b/src/ui/common/UIImage.ts @@ -1,7 +1,6 @@ -module TK.SpaceTac.UI { - /** - * UI component to display an image - */ - export class UIImage extends Phaser.GameObjects.Image { - } +/** + * UI component to display an image + */ +export class UIImage extends Phaser.GameObjects.Image { } + diff --git a/src/ui/common/UIOverlay.ts b/src/ui/common/UIOverlay.ts index 88ac9af..7bb560b 100644 --- a/src/ui/common/UIOverlay.ts +++ b/src/ui/common/UIOverlay.ts @@ -1,24 +1,26 @@ -module TK.SpaceTac.UI { - export interface UIOverlayOptions { - color: number, - alpha?: number, - on_click?: Function - } +import { nop } from "../../common/Tools"; +import { BaseView } from "../BaseView"; +import { UIGraphics } from "./UIGraphics"; - /** - * UI component to display a semi-transparent overlay that fills the whole view and captures inputs - */ - export class UIOverlay extends UIGraphics { - constructor(view: BaseView, options: UIOverlayOptions) { - super(view, "overlay"); - - let rect = { x: 0, y: 0, width: view.getWidth(), height: view.getHeight() }; - this.addRectangle(rect, options.color, undefined, undefined, options.alpha); - this.setInteractive({ - hitArea: rect, - hitAreaCallback: Phaser.Geom.Rectangle.Contains, - }); - this.on("pointerup", options.on_click || nop); - } - } +export interface UIOverlayOptions { + color: number, + alpha?: number, + on_click?: Function +} + +/** + * UI component to display a semi-transparent overlay that fills the whole view and captures inputs + */ +export class UIOverlay extends UIGraphics { + constructor(view: BaseView, options: UIOverlayOptions) { + super(view, "overlay"); + + let rect = { x: 0, y: 0, width: view.getWidth(), height: view.getHeight() }; + this.addRectangle(rect, options.color, undefined, undefined, options.alpha); + this.setInteractive({ + hitArea: rect, + hitAreaCallback: Phaser.Geom.Rectangle.Contains, + }); + this.on("pointerup", options.on_click || nop); + } } diff --git a/src/ui/common/UIParticles.ts b/src/ui/common/UIParticles.ts index ce238da..1b43384 100644 --- a/src/ui/common/UIParticles.ts +++ b/src/ui/common/UIParticles.ts @@ -1,121 +1,123 @@ -module TK.SpaceTac.UI { - export enum ParticleFacingMode { - INITIAL = 1, - ALWAYS = 2 +import { degrees } from "../../core/ArenaLocation"; +import { BaseView } from "../BaseView"; +import { UIContainer } from "./UIContainer"; + +export enum ParticleFacingMode { + INITIAL = 1, + ALWAYS = 2 +} + +export type ParticlesConfig = { + // Key for the particle texture + key: string, + // Source of the particles + source: { x: number, y: number, radius: number }, + // Total number of particles to emit + count: number, + // Duration of the emission of particles + emitDuration: number + // Lifespan of a single particle in milliseconds + lifetime: number, + // Fade the alpha during the lifespan + fading?: boolean, + // Direction of the particles for radial emission + direction: { minangle: number, maxangle: number }, + // Scale of the emitted particles + scale: { min: number, max: number } + // Speed of the particles + speed: { min: number, max: number } + // Force the particle to face its direction + facing?: ParticleFacingMode +} + +/** + * Override of phaser particle manager to fix some issues + */ +export class UIParticleManager extends Phaser.GameObjects.Particles.ParticleEmitterManager { + setScrollFactor() { + } + setAlpha() { + } +} + +/** + * System to emit multiple particles of the same texture + */ +export class UIParticles { + constructor(private view: BaseView) { + } + + /** + * Emit a batch of particles + * + * Returns the total duration in milliseconds + */ + emit(config: ParticlesConfig, parent?: UIContainer): number { + let manager = this.createManager(config.key, parent); + let emitter = manager.createEmitter({}); + if (config.fading) { + emitter.setAlpha({ start: 1, end: 0 }); } - - export type ParticlesConfig = { - // Key for the particle texture - key: string, - // Source of the particles - source: { x: number, y: number, radius: number }, - // Total number of particles to emit - count: number, - // Duration of the emission of particles - emitDuration: number - // Lifespan of a single particle in milliseconds - lifetime: number, - // Fade the alpha during the lifespan - fading?: boolean, - // Direction of the particles for radial emission - direction: { minangle: number, maxangle: number }, - // Scale of the emitted particles - scale: { min: number, max: number } - // Speed of the particles - speed: { min: number, max: number } - // Force the particle to face its direction - facing?: ParticleFacingMode + emitter.setPosition( + { min: config.source.x - config.source.radius, max: config.source.x + config.source.radius }, + { min: config.source.y - config.source.radius, max: config.source.y + config.source.radius }, + ); + emitter.setSpeed({ min: config.speed.min, max: config.speed.max }); + emitter.setRadial(true); + emitter.setEmitterAngle({ min: degrees(config.direction.minangle), max: degrees(config.direction.maxangle) }); + emitter.setLifespan(config.lifetime); + emitter.setFrequency(config.emitDuration / config.count, 1); + emitter.setScale({ min: config.scale.min, max: config.scale.max }); + if (config.facing) { + emitter.particleClass = ((config.facing == ParticleFacingMode.ALWAYS) ? FacingAlwaysParticle : FacingInitialParticle); } + this.view.timer.schedule(config.emitDuration, () => emitter.on = false); + this.view.timer.schedule(config.emitDuration + config.lifetime, () => manager.destroy()); + return config.emitDuration + config.lifetime; + } - /** - * Override of phaser particle manager to fix some issues - */ - export class UIParticleManager extends Phaser.GameObjects.Particles.ParticleEmitterManager { - setScrollFactor() { - } - setAlpha() { - } + /** + * Async version of *emit* + */ + emit_as(config: ParticlesConfig): Promise { + let duration = this.emit(config); + return this.view.timer.sleep(duration); + } + + /** + * Create a new particle manager + * + * Automatically called by *emit*. + */ + createManager(key: string, parent?: UIContainer): UIParticleManager { + let info = this.view.getImageInfo(key); + let result = new UIParticleManager(this.view, info.key, info.frame, []); + this.view.add.existing(result); + if (parent) { + parent.add(result); } + return result; + } +} - /** - * System to emit multiple particles of the same texture - */ - export class UIParticles { - constructor(private view: BaseView) { - } +/** + * Particle that is rotated to face its initial direction + */ +export class FacingInitialParticle extends Phaser.GameObjects.Particles.Particle { + fire(x: number, y: number): any { + let result = super.fire(x, y); + this.rotation = Math.atan2(this.velocityY, this.velocityX); + return result; + } +} - /** - * Emit a batch of particles - * - * Returns the total duration in milliseconds - */ - emit(config: ParticlesConfig, parent?: UIContainer): number { - let manager = this.createManager(config.key, parent); - let emitter = manager.createEmitter({}); - if (config.fading) { - emitter.setAlpha({ start: 1, end: 0 }); - } - emitter.setPosition( - { min: config.source.x - config.source.radius, max: config.source.x + config.source.radius }, - { min: config.source.y - config.source.radius, max: config.source.y + config.source.radius }, - ); - emitter.setSpeed({ min: config.speed.min, max: config.speed.max }); - emitter.setRadial(true); - emitter.setEmitterAngle({ min: degrees(config.direction.minangle), max: degrees(config.direction.maxangle) }); - emitter.setLifespan(config.lifetime); - emitter.setFrequency(config.emitDuration / config.count, 1); - emitter.setScale({ min: config.scale.min, max: config.scale.max }); - if (config.facing) { - emitter.particleClass = ((config.facing == ParticleFacingMode.ALWAYS) ? FacingAlwaysParticle : FacingInitialParticle); - } - this.view.timer.schedule(config.emitDuration, () => emitter.on = false); - this.view.timer.schedule(config.emitDuration + config.lifetime, () => manager.destroy()); - return config.emitDuration + config.lifetime; - } - - /** - * Async version of *emit* - */ - emit_as(config: ParticlesConfig): Promise { - let duration = this.emit(config); - return this.view.timer.sleep(duration); - } - - /** - * Create a new particle manager - * - * Automatically called by *emit*. - */ - createManager(key: string, parent?: UIContainer): UIParticleManager { - let info = this.view.getImageInfo(key); - let result = new UIParticleManager(this.view, info.key, info.frame, []); - this.view.add.existing(result); - if (parent) { - parent.add(result); - } - return result; - } - } - - /** - * Particle that is rotated to face its initial direction - */ - export class FacingInitialParticle extends Phaser.GameObjects.Particles.Particle { - fire(x: number, y: number): any { - let result = super.fire(x, y); - this.rotation = Math.atan2(this.velocityY, this.velocityX); - return result; - } - } - - /** - * Particle that is rotated to face its movement direction - */ - export class FacingAlwaysParticle extends FacingInitialParticle { - update(delta: any, step: any, processors: any): any { - let result = super.update(delta, step, processors); - this.rotation = Math.atan2(this.velocityY, this.velocityX); - return result; - } - } -} \ No newline at end of file +/** + * Particle that is rotated to face its movement direction + */ +export class FacingAlwaysParticle extends FacingInitialParticle { + update(delta: any, step: any, processors: any): any { + let result = super.update(delta, step, processors); + this.rotation = Math.atan2(this.velocityY, this.velocityX); + return result; + } +} diff --git a/src/ui/common/UIText.ts b/src/ui/common/UIText.ts index 395f6e7..71bd730 100644 --- a/src/ui/common/UIText.ts +++ b/src/ui/common/UIText.ts @@ -1,7 +1,5 @@ -module TK.SpaceTac.UI { - /** - * UI component to display a text - */ - export class UIText extends Phaser.GameObjects.Text { - } +/** + * UI component to display a text + */ +export class UIText extends Phaser.GameObjects.Text { } diff --git a/src/ui/common/UITextDialog.ts b/src/ui/common/UITextDialog.ts index ae2d7f5..83b0584 100644 --- a/src/ui/common/UITextDialog.ts +++ b/src/ui/common/UITextDialog.ts @@ -1,50 +1,52 @@ -module TK.SpaceTac.UI { - /** - * Dialog asking for a text input - */ - export class UITextDialog extends UIDialog { - private result: Promise - private result_resolver?: (input: string | null) => void +import { BaseView } from "../BaseView"; +import { UIDialog } from "./UIDialog"; +import { UITextInput } from "./UITextInput"; - constructor(view: BaseView, message: string, initial?: string) { - super(view); +/** + * Dialog asking for a text input + */ +export class UITextDialog extends UIDialog { + private result: Promise + private result_resolver?: (input: string | null) => void - this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#9FC4D6", size: 32, shadow: true }); + constructor(view: BaseView, message: string, initial?: string) { + super(view); - let input = new UITextInput(this.content.styled({ size: 26, color: "#DBEFF9", shadow: true }), "menu-input", this.width / 2, this.height / 2, 12); - if (initial) { - input.setContent(initial); - } + this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#9FC4D6", size: 32, shadow: true }); - this.result = new Promise(resolve => { - this.result_resolver = resolve; - this.content.button("menu-button-small", this.width * 0.4, this.height * 0.7, () => resolve(null), - undefined, undefined, { center: true, text: "Cancel", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); - this.content.button("menu-button-small", this.width * 0.6, this.height * 0.7, () => resolve(input.getContent()), - undefined, undefined, { center: true, text: "OK", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); - }); - } - - /** - * Force the result (simulate filling the input and validation) - */ - async forceResult(input: string | null): Promise { - if (this.result_resolver) { - this.result_resolver(input); - await this.result; - } - } - - /** - * Convenient function to ask for an input, and have a promise of result - */ - static ask(view: BaseView, message: string, initial?: string): Promise { - let dlg = new UITextDialog(view, message, initial); - let result = dlg.result; - return result.then(confirmed => { - dlg.close(); - return confirmed; - }); - } + let input = new UITextInput(this.content.styled({ size: 26, color: "#DBEFF9", shadow: true }), "menu-input", this.width / 2, this.height / 2, 12); + if (initial) { + input.setContent(initial); } -} \ No newline at end of file + + this.result = new Promise(resolve => { + this.result_resolver = resolve; + this.content.button("menu-button-small", this.width * 0.4, this.height * 0.7, () => resolve(null), + undefined, undefined, { center: true, text: "Cancel", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); + this.content.button("menu-button-small", this.width * 0.6, this.height * 0.7, () => resolve(input.getContent()), + undefined, undefined, { center: true, text: "OK", text_style: { color: "#9FC4D6", size: 22, shadow: true } }); + }); + } + + /** + * Force the result (simulate filling the input and validation) + */ + async forceResult(input: string | null): Promise { + if (this.result_resolver) { + this.result_resolver(input); + await this.result; + } + } + + /** + * Convenient function to ask for an input, and have a promise of result + */ + static ask(view: BaseView, message: string, initial?: string): Promise { + let dlg = new UITextDialog(view, message, initial); + let result = dlg.result; + return result.then(confirmed => { + dlg.close(); + return confirmed; + }); + } +} diff --git a/src/ui/common/UITextInput.ts b/src/ui/common/UITextInput.ts index 24ccff7..d538fe9 100644 --- a/src/ui/common/UITextInput.ts +++ b/src/ui/common/UITextInput.ts @@ -1,48 +1,50 @@ -module TK.SpaceTac.UI { - /** - * UI component to allow the user to enter a small text - */ - export class UITextInput { - private container: UIButton - private content: UIText - private placeholder: UIText - private maxlength: number +import { UIBuilder } from "./UIBuilder"; +import { UIButton } from "./UIButton"; +import { UIText } from "./UIText"; - constructor(builder: UIBuilder, background: string, x = 0, y = 0, maxlength: number, placeholder = "") { - this.container = builder.button(background, x, y, () => { - builder.view.inputs.grabKeyboard(this, key => this.processKey(key)); - }, undefined, undefined, { center: true }); +/** + * UI component to allow the user to enter a small text + */ +export class UITextInput { + private container: UIButton + private content: UIText + private placeholder: UIText + private maxlength: number - this.content = builder.in(this.container).text("", 0, 0, { center: true }); - this.placeholder = builder.in(this.container).text(placeholder, 0, 0, { center: true }); - this.placeholder.setAlpha(0.5); - this.maxlength = maxlength; - } + constructor(builder: UIBuilder, background: string, x = 0, y = 0, maxlength: number, placeholder = "") { + this.container = builder.button(background, x, y, () => { + builder.view.inputs.grabKeyboard(this, key => this.processKey(key)); + }, undefined, undefined, { center: true }); - /** - * Process a key press - */ - processKey(key: string): void { - if (key.length == 1 && this.content.text.length < this.maxlength) { - this.setContent(this.content.text + key); - } else if (key == "Backspace" && this.content.text.length > 0) { - this.setContent(this.content.text.substr(0, this.content.text.length - 1)); - } - } + this.content = builder.in(this.container).text("", 0, 0, { center: true }); + this.placeholder = builder.in(this.container).text(placeholder, 0, 0, { center: true }); + this.placeholder.setAlpha(0.5); + this.maxlength = maxlength; + } - /** - * Get current text content - */ - getContent(): string { - return this.content.text; - } - - /** - * Set the current text content - */ - setContent(content: string): void { - this.content.setText(content.slice(0, this.maxlength)); - this.placeholder.setVisible(!this.content.text); - } + /** + * Process a key press + */ + processKey(key: string): void { + if (key.length == 1 && this.content.text.length < this.maxlength) { + this.setContent(this.content.text + key); + } else if (key == "Backspace" && this.content.text.length > 0) { + this.setContent(this.content.text.substr(0, this.content.text.length - 1)); } -} \ No newline at end of file + } + + /** + * Get current text content + */ + getContent(): string { + return this.content.text; + } + + /** + * Set the current text content + */ + setContent(content: string): void { + this.content.setText(content.slice(0, this.maxlength)); + this.placeholder.setVisible(!this.content.text); + } +} diff --git a/src/ui/common/UITools.spec.ts b/src/ui/common/UITools.spec.ts index aba312d..9d50e0d 100644 --- a/src/ui/common/UITools.spec.ts +++ b/src/ui/common/UITools.spec.ts @@ -1,135 +1,138 @@ -module TK.SpaceTac.UI.Specs { - testing("UITools", test => { - testing("in UI", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { UIBuilder } from "./UIBuilder"; +import { destroyChildren, UITools } from "./UITools"; - test.case("destroys children", check => { - let builder = new UIBuilder(testgame.view); - let parent = builder.container("group"); - let child1 = builder.in(parent).graphics("graphics"); - let child2 = builder.in(parent).image("image"); - let child3 = builder.in(parent).button("button"); - let child4 = builder.in(parent).text(""); - check.equals(parent.length, 4); +testing("UITools", test => { + testing("in UI", test => { + let testgame = setupEmptyView(test); - destroyChildren(parent, 1, 2); - check.equals(parent.length, 2); + test.case("destroys children", check => { + let builder = new UIBuilder(testgame.view); + let parent = builder.container("group"); + let child1 = builder.in(parent).graphics("graphics"); + let child2 = builder.in(parent).image("image"); + let child3 = builder.in(parent).button("button"); + let child4 = builder.in(parent).text(""); + check.equals(parent.length, 4); - destroyChildren(parent); - check.equals(parent.length, 0); - }); + destroyChildren(parent, 1, 2); + check.equals(parent.length, 2); - /*test.case("gets the screen boundaries of an object", check => { - let builder = new UIBuilder(testgame.view); - let parent = builder.group("group"); + destroyChildren(parent); + check.equals(parent.length, 0); + }); - check.in("empty", check => { - check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); - }); + /*test.case("gets the screen boundaries of an object", check => { + let builder = new UIBuilder(testgame.view); + let parent = builder.group("group"); - let child1 = builder.in(parent).graphics("child1"); - child1.setPosition(10, 20); - - check.in("empty child", check => { - check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 0, y: 0, width: 0, height: 0 }, "child1"); - }); - - child1.addRectangle({ x: 20, y: 30, width: 40, height: 45 }, 0); - - check.in("rectangle child", check => { - check.containing(UITools.getBounds(parent), { x: 30, y: 50, width: 40, height: 45 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 30, y: 50, width: 40, height: 45 }, "child1"); - }); - - child1.setScale(0.5, 0.2); - - check.in("scaled child", check => { - check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); - }); - - let child2 = testgame.view.add.graphics(-4, -15); - child1.addChild(child2); - - check.in("sub child empty", check => { - check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); - check.containing(UITools.getBounds(child2), { x: 0, y: 0, width: 0, height: 0 }, "child2"); - }); - - child2.drawRect(0, 0, 10, 5); - - check.in("sub child rectangle", check => { - check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 32, height: 18 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); - check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); - }); - - let child3 = testgame.view.add.graphics(50, 51, parent); - - check.in("second child empty", check => { - check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 42, height: 34 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); - check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); - check.containing(UITools.getBounds(child3), { x: 0, y: 0, width: 0, height: 0 }, "child3"); - }); - - child3.drawRect(1, 1, 1, 1); - - check.in("second child pixel", check => { - check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 44, height: 36 }, "parent"); - check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); - check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); - check.containing(UITools.getBounds(child3), { x: 51, y: 52, width: 1, height: 1 }, "child3"); - }); - }); - - test.case("keeps objects inside bounds", check => { - let builder = new UIBuilder(testgame.view); - let image = builder.graphics("test", 150, 100, true); - image.beginFill(0xff0000); - image.drawEllipse(50, 25, 50, 25); - image.endFill(); - - UITools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 }); - - check.equals(image.x, 100); - check.equals(image.y, 100); - }); - - test.case("draws a rectangle background around content", check => { - let group = testgame.view.add.group(); - - let content = testgame.view.add.graphics(0, 0, group); - content.drawRect(120, 90, 30, 20); - - let background = testgame.view.add.graphics(0, 0); - - let result = UITools.drawBackground(group, background, 3); - check.equals(result, [36, 26]); - - content.drawCircle(0, 0, 50); - result = UITools.drawBackground(group, background, 3); - check.equals(result, [181, 141]); - });*/ + check.in("empty", check => { + check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); }); - test.case("normalizes angles", check => { - check.equals(UITools.normalizeAngle(0), 0); - check.nears(UITools.normalizeAngle(0.1), 0.1); - check.nears(UITools.normalizeAngle(Math.PI), Math.PI); - check.nears(UITools.normalizeAngle(Math.PI + 0.5), -Math.PI + 0.5); - check.nears(UITools.normalizeAngle(-Math.PI), Math.PI); - check.nears(UITools.normalizeAngle(-Math.PI - 0.5), Math.PI - 0.5); + let child1 = builder.in(parent).graphics("child1"); + child1.setPosition(10, 20); + + check.in("empty child", check => { + check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 0, y: 0, width: 0, height: 0 }, "child1"); }); - test.case("spaces items evenly", check => { - check.equals(UITools.evenlySpace(100, 20, 0), []); - check.equals(UITools.evenlySpace(100, 20, 1), [50]); - check.equals(UITools.evenlySpace(100, 20, 2), [25, 75]); - check.equals(UITools.evenlySpace(100, 20, 5), [10, 30, 50, 70, 90]); - check.equals(UITools.evenlySpace(100, 20, 6), [10, 26, 42, 58, 74, 90]); + child1.addRectangle({ x: 20, y: 30, width: 40, height: 45 }, 0); + + check.in("rectangle child", check => { + check.containing(UITools.getBounds(parent), { x: 30, y: 50, width: 40, height: 45 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 30, y: 50, width: 40, height: 45 }, "child1"); + }); + + child1.setScale(0.5, 0.2); + + check.in("scaled child", check => { + check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); + }); + + let child2 = testgame.view.add.graphics(-4, -15); + child1.addChild(child2); + + check.in("sub child empty", check => { + check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); + check.containing(UITools.getBounds(child2), { x: 0, y: 0, width: 0, height: 0 }, "child2"); + }); + + child2.drawRect(0, 0, 10, 5); + + check.in("sub child rectangle", check => { + check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 32, height: 18 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + }); + + let child3 = testgame.view.add.graphics(50, 51, parent); + + check.in("second child empty", check => { + check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 42, height: 34 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + check.containing(UITools.getBounds(child3), { x: 0, y: 0, width: 0, height: 0 }, "child3"); + }); + + child3.drawRect(1, 1, 1, 1); + + check.in("second child pixel", check => { + check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 44, height: 36 }, "parent"); + check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + check.containing(UITools.getBounds(child3), { x: 51, y: 52, width: 1, height: 1 }, "child3"); }); }); -} + + test.case("keeps objects inside bounds", check => { + let builder = new UIBuilder(testgame.view); + let image = builder.graphics("test", 150, 100, true); + image.beginFill(0xff0000); + image.drawEllipse(50, 25, 50, 25); + image.endFill(); + + UITools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 }); + + check.equals(image.x, 100); + check.equals(image.y, 100); + }); + + test.case("draws a rectangle background around content", check => { + let group = testgame.view.add.group(); + + let content = testgame.view.add.graphics(0, 0, group); + content.drawRect(120, 90, 30, 20); + + let background = testgame.view.add.graphics(0, 0); + + let result = UITools.drawBackground(group, background, 3); + check.equals(result, [36, 26]); + + content.drawCircle(0, 0, 50); + result = UITools.drawBackground(group, background, 3); + check.equals(result, [181, 141]); + });*/ + }); + + test.case("normalizes angles", check => { + check.equals(UITools.normalizeAngle(0), 0); + check.nears(UITools.normalizeAngle(0.1), 0.1); + check.nears(UITools.normalizeAngle(Math.PI), Math.PI); + check.nears(UITools.normalizeAngle(Math.PI + 0.5), -Math.PI + 0.5); + check.nears(UITools.normalizeAngle(-Math.PI), Math.PI); + check.nears(UITools.normalizeAngle(-Math.PI - 0.5), Math.PI - 0.5); + }); + + test.case("spaces items evenly", check => { + check.equals(UITools.evenlySpace(100, 20, 0), []); + check.equals(UITools.evenlySpace(100, 20, 1), [50]); + check.equals(UITools.evenlySpace(100, 20, 2), [25, 75]); + check.equals(UITools.evenlySpace(100, 20, 5), [10, 30, 50, 70, 90]); + check.equals(UITools.evenlySpace(100, 20, 6), [10, 26, 42, 58, 74, 90]); + }); +}); diff --git a/src/ui/common/UITools.ts b/src/ui/common/UITools.ts index 5e96d0d..af56c3d 100644 --- a/src/ui/common/UITools.ts +++ b/src/ui/common/UITools.ts @@ -1,169 +1,174 @@ -module TK.SpaceTac.UI { - export type IBounded = { - x: number - y: number - width: number - height: number - } +import { range } from "../../common/Tools"; +import { UIBackground } from "./UIBackground"; +import { UIButton } from "./UIButton"; +import { UIContainer } from "./UIContainer"; +import { UIImage } from "./UIImage"; +import { UIText } from "./UIText"; - /** - * Destroy all children of a Phaser object - * - * This is a workaround for a removeChildren bug - */ - export function destroyChildren(obj: UIContainer, start = 0, end = obj.length - 1) { - obj.list.slice(start, end + 1).forEach(child => child.destroy()); - } - - /** - * Common UI function to work around some Phaser limitations - */ - export class UITools { - /** - * Check that a game object has transform and bounds available - */ - static isSpatial(obj: any): obj is Phaser.GameObjects.Components.GetBounds & Phaser.GameObjects.Components.Transform { - return obj instanceof UIImage || obj instanceof UIText || obj instanceof UIContainer; - } - - /** - * Get the bounding rectanle of a displayed object, in screen space - */ - static getBounds(obj: UIContainer | (Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.GetBounds)): IBounded { - let result: IBounded; - - if (obj instanceof UIContainer) { - result = obj.getBounds(); - } else { - result = obj.getBounds(); - } - - return result; - } - - /** - * Check if a game object is visible - */ - static isVisible(obj: Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Visible & Phaser.GameObjects.Components.Alpha): boolean { - if (obj.visible && obj.alpha) { - if (obj.parentContainer) { - return this.isVisible(obj.parentContainer); - } else { - return true; - } - } else { - return false; - } - } - - /** - * Get the position of an object, adjusted to remain inside a container - */ - static positionInside(obj: IBounded, container: IBounded): [number, number] { - let y = obj.y; - if (y + obj.height > container.height) { - y = container.height - obj.height; - } - if (y < container.y) { - y = container.y; - } - - let x = obj.x; - if (x + obj.width > container.width) { - x = container.width - obj.width; - } - if (x < container.x) { - x = container.x; - } - - return [x, y]; - } - - /** - * Reposition an object to remain inside a container - */ - static keepInside(obj: UIButton | UIImage | UIContainer, rect: IBounded) { - let objbounds = UITools.getBounds(obj); - let [x, y] = UITools.positionInside({ x: obj.x, y: obj.y, width: objbounds.width, height: objbounds.height }, rect); - - if (x != obj.x || y != obj.y) { - obj.setPosition(x, y); - } - } - - /** - * Compare two rectangles - */ - static compareRects(rct1: IBounded, rct2: IBounded) { - return rct1.x == rct2.x && rct1.y == rct2.y && rct1.width == rct2.width && rct1.height == rct2.height; - } - - /** - * Returns the bounding rectangle containing two other rectangles - */ - static unionRects(rct1: IBounded, rct2: IBounded): IBounded { - let result: IBounded; - if (rct1.width == 0 || rct1.height == 0) { - result = rct2; - } else if (rct2.width == 0 || rct2.height == 0) { - result = rct1; - } else { - let xmin = Math.min(rct1.x, rct2.x); - let xmax = Math.max(rct1.x + rct1.width, rct2.x + rct2.width); - let ymin = Math.min(rct1.y, rct2.y); - let ymax = Math.max(rct1.y + rct1.height, rct2.y + rct2.height); - - result = { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; - } - - if (result.width == 0 || result.height == 0) { - return { x: 0, y: 0, width: 0, height: 0 }; - } else { - return result; - } - } - - /** - * Constraint an angle in radians the ]-pi;pi] range. - */ - static normalizeAngle(angle: number): number { - angle = angle % (2 * Math.PI); - if (angle <= -Math.PI) { - return angle + 2 * Math.PI; - } else if (angle > Math.PI) { - return angle - 2 * Math.PI; - } else { - return angle; - } - } - - /** - * Evenly space identical items in a parent - * - * Returns the relative position of item's center inside parent_width - */ - static evenlySpace(parent_width: number, item_width: number, item_count: number): number[] { - if (item_width * item_count <= parent_width) { - let spacing = parent_width / item_count; - return range(item_count).map(i => (i + 0.5) * spacing); - } else { - let breadth = parent_width - item_width; - let spacing = breadth / (item_count - 1); - return range(item_count).map(i => item_width / 2 + i * spacing); - } - } - - /** - * Draw a background around a content - */ - static drawBackground(content: UIContainer | UIText, background: UIBackground, border = 6): [number, number] { - if (content.parentContainer === background.parent) { - background.adaptToContent(content); - return [background.width, background.height]; - } else { - console.error("Cannot draw background with different parents", content, background); - return [0, 0]; - } - } - } +export type IBounded = { + x: number + y: number + width: number + height: number +} + +/** + * Destroy all children of a Phaser object + * + * This is a workaround for a removeChildren bug + */ +export function destroyChildren(obj: UIContainer, start = 0, end = obj.length - 1) { + obj.list.slice(start, end + 1).forEach(child => child.destroy()); +} + +/** + * Common UI function to work around some Phaser limitations + */ +export class UITools { + /** + * Check that a game object has transform and bounds available + */ + static isSpatial(obj: any): obj is Phaser.GameObjects.Components.GetBounds & Phaser.GameObjects.Components.Transform { + return obj instanceof UIImage || obj instanceof UIText || obj instanceof UIContainer; + } + + /** + * Get the bounding rectanle of a displayed object, in screen space + */ + static getBounds(obj: UIContainer | (Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.GetBounds)): IBounded { + let result: IBounded; + + if (obj instanceof UIContainer) { + result = obj.getBounds(); + } else { + result = obj.getBounds(); + } + + return result; + } + + /** + * Check if a game object is visible + */ + static isVisible(obj: Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Visible & Phaser.GameObjects.Components.Alpha): boolean { + if (obj.visible && obj.alpha) { + if (obj.parentContainer) { + return this.isVisible(obj.parentContainer); + } else { + return true; + } + } else { + return false; + } + } + + /** + * Get the position of an object, adjusted to remain inside a container + */ + static positionInside(obj: IBounded, container: IBounded): [number, number] { + let y = obj.y; + if (y + obj.height > container.height) { + y = container.height - obj.height; + } + if (y < container.y) { + y = container.y; + } + + let x = obj.x; + if (x + obj.width > container.width) { + x = container.width - obj.width; + } + if (x < container.x) { + x = container.x; + } + + return [x, y]; + } + + /** + * Reposition an object to remain inside a container + */ + static keepInside(obj: UIButton | UIImage | UIContainer, rect: IBounded) { + let objbounds = UITools.getBounds(obj); + let [x, y] = UITools.positionInside({ x: obj.x, y: obj.y, width: objbounds.width, height: objbounds.height }, rect); + + if (x != obj.x || y != obj.y) { + obj.setPosition(x, y); + } + } + + /** + * Compare two rectangles + */ + static compareRects(rct1: IBounded, rct2: IBounded) { + return rct1.x == rct2.x && rct1.y == rct2.y && rct1.width == rct2.width && rct1.height == rct2.height; + } + + /** + * Returns the bounding rectangle containing two other rectangles + */ + static unionRects(rct1: IBounded, rct2: IBounded): IBounded { + let result: IBounded; + if (rct1.width == 0 || rct1.height == 0) { + result = rct2; + } else if (rct2.width == 0 || rct2.height == 0) { + result = rct1; + } else { + let xmin = Math.min(rct1.x, rct2.x); + let xmax = Math.max(rct1.x + rct1.width, rct2.x + rct2.width); + let ymin = Math.min(rct1.y, rct2.y); + let ymax = Math.max(rct1.y + rct1.height, rct2.y + rct2.height); + + result = { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; + } + + if (result.width == 0 || result.height == 0) { + return { x: 0, y: 0, width: 0, height: 0 }; + } else { + return result; + } + } + + /** + * Constraint an angle in radians the ]-pi;pi] range. + */ + static normalizeAngle(angle: number): number { + angle = angle % (2 * Math.PI); + if (angle <= -Math.PI) { + return angle + 2 * Math.PI; + } else if (angle > Math.PI) { + return angle - 2 * Math.PI; + } else { + return angle; + } + } + + /** + * Evenly space identical items in a parent + * + * Returns the relative position of item's center inside parent_width + */ + static evenlySpace(parent_width: number, item_width: number, item_count: number): number[] { + if (item_width * item_count <= parent_width) { + let spacing = parent_width / item_count; + return range(item_count).map(i => (i + 0.5) * spacing); + } else { + let breadth = parent_width - item_width; + let spacing = breadth / (item_count - 1); + return range(item_count).map(i => item_width / 2 + i * spacing); + } + } + + /** + * Draw a background around a content + */ + static drawBackground(content: UIContainer | UIText, background: UIBackground, border = 6): [number, number] { + if (content.parentContainer === background.parent) { + background.adaptToContent(content); + return [background.width, background.height]; + } else { + console.error("Cannot draw background with different parents", content, background); + return [0, 0]; + } + } } diff --git a/src/ui/common/UIWaitingDialog.ts b/src/ui/common/UIWaitingDialog.ts index b87e0ef..c7d5d33 100644 --- a/src/ui/common/UIWaitingDialog.ts +++ b/src/ui/common/UIWaitingDialog.ts @@ -1,21 +1,22 @@ -module TK.SpaceTac.UI { - /** - * Dialog with a waiting indicator - */ - export class UIWaitingDialog extends UIDialog { - constructor(view: BaseView, message: string, cancel?: Function) { - super(view); +import { BaseView } from "../BaseView"; +import { UIDialog } from "./UIDialog"; - this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#9FC4D6", size: 32 }); - this.content.awaiter(this.width * 0.5, this.height * 0.6); - } +/** + * Dialog with a waiting indicator + */ +export class UIWaitingDialog extends UIDialog { + constructor(view: BaseView, message: string, cancel?: Function) { + super(view); - /** - * Display an error as the result of waiting. - */ - displayError(message: string) { - this.content.clear(); - this.content.text(message, this.width * 0.5, this.height * 0.5, { color: "#FFDFBA", size: 32 }); - } - } -} \ No newline at end of file + this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#9FC4D6", size: 32 }); + this.content.awaiter(this.width * 0.5, this.height * 0.6); + } + + /** + * Display an error as the result of waiting. + */ + displayError(message: string) { + this.content.clear(); + this.content.text(message, this.width * 0.5, this.height * 0.5, { color: "#FFDFBA", size: 32 }); + } +} diff --git a/src/ui/common/ValueBar.spec.ts b/src/ui/common/ValueBar.spec.ts index c5ffd7b..52fb6fd 100644 --- a/src/ui/common/ValueBar.spec.ts +++ b/src/ui/common/ValueBar.spec.ts @@ -1,19 +1,21 @@ -module TK.SpaceTac.UI.Specs { - testing("ValueBar", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { setupEmptyView } from "../TestGame"; +import { ValueBar, ValueBarOrientation } from "./ValueBar"; - test.case("computes proportional value", check => { - var bar = new ValueBar(testgame.view, "default", ValueBarOrientation.EAST); - check.equals(bar.getProportionalValue(), 0); +testing("ValueBar", test => { + let testgame = setupEmptyView(test); - bar.setValue(20, 100); - check.nears(bar.getProportionalValue(), 0.2); + test.case("computes proportional value", check => { + var bar = new ValueBar(testgame.view, "default", ValueBarOrientation.EAST); + check.equals(bar.getProportionalValue(), 0); - bar.setValue(40); - check.nears(bar.getProportionalValue(), 0.4); + bar.setValue(20, 100); + check.nears(bar.getProportionalValue(), 0.2); - bar.setValue(0, 0); - check.equals(bar.getProportionalValue(), 0); - }); - }); -} + bar.setValue(40); + check.nears(bar.getProportionalValue(), 0.4); + + bar.setValue(0, 0); + check.equals(bar.getProportionalValue(), 0); + }); +}); diff --git a/src/ui/common/ValueBar.ts b/src/ui/common/ValueBar.ts index a136330..377d388 100644 --- a/src/ui/common/ValueBar.ts +++ b/src/ui/common/ValueBar.ts @@ -1,120 +1,121 @@ -module TK.SpaceTac.UI { - /** - * Orientation of a ValueBar. - * - * A EAST bar will have 0 at the west, and 1 at the east. - */ - export enum ValueBarOrientation { - NORTH, - SOUTH, - EAST, - WEST, - } +import { BaseView } from "../BaseView" +import { UIImage } from "./UIImage" - /** - * Bar to display a value with a graphical bar - * - * This will crop the image according to the value - */ - export class ValueBar { - // Phaser node - node: UIImage - - // Orientation - private orientation: ValueBarOrientation - - // Current value - private current = 0 - - // Maximal value - private maximal = 0 - - // Proportional value - private proportional = 0 - - // Original size - private original_width: number - private original_height: number - private crop_rect: Phaser.Geom.Rectangle - private crop_mask: Phaser.GameObjects.Graphics - - constructor(view: BaseView, name: string, orientation: ValueBarOrientation, x = 0, y = 0) { - this.node = view.newImage(name, x, y); - if (orientation == ValueBarOrientation.WEST) { - this.node.setOrigin(1, 0); - } else if (orientation == ValueBarOrientation.NORTH) { - this.node.setOrigin(0, 1); - } else { - this.node.setOrigin(0, 0); - } - - this.orientation = orientation; - this.original_width = this.node.width; - this.original_height = this.node.height; - - this.crop_rect = new Phaser.Geom.Rectangle(0, 0, this.original_width, this.original_height); - this.crop_mask = view.make.graphics({ x: x, y: y, add: false }); - this.crop_mask.fillStyle(0xffffff); - this.node.setMask(new Phaser.Display.Masks.GeometryMask(view, this.crop_mask)); - - this.setValue(0, 1000); - } - - /** - * Update the phaser graphics to match the value - */ - update() { - // TODO animation - switch (this.orientation) { - case ValueBarOrientation.EAST: - this.crop_rect.width = Math.round(this.original_width * this.proportional); - break; - case ValueBarOrientation.WEST: - this.crop_rect.width = Math.round(this.original_width * this.proportional); - this.crop_rect.x = this.original_width - this.crop_rect.width; - break; - case ValueBarOrientation.NORTH: - this.crop_rect.height = Math.round(this.original_height * this.proportional); - this.crop_rect.y = this.original_height - this.crop_rect.height; - break; - case ValueBarOrientation.SOUTH: - this.crop_rect.height = Math.round(this.original_height * this.proportional); - break; - } - - this.crop_mask.clear(); - this.crop_mask.fillRectShape(this.crop_rect); - } - - /** - * Set the current value, and maximal value - */ - setValue(current: number, maximal: number = -1) { - this.current = current > 0 ? current : 0; - if (maximal >= 0) { - this.maximal = maximal; - } - if (this.maximal === 0) { - this.proportional = 0; - } else { - this.proportional = this.current / this.maximal; - } - - this.update(); - } - - /** - * Get current raw value - */ - getValue(): number { - return this.current; - } - - /** - * Get the proportional (in 0.0-1.0 range) value - */ - getProportionalValue(): number { - return this.proportional; - } - } +/** + * Orientation of a ValueBar. + * + * A EAST bar will have 0 at the west, and 1 at the east. + */ +export enum ValueBarOrientation { + NORTH, + SOUTH, + EAST, + WEST, +} + +/** + * Bar to display a value with a graphical bar + * + * This will crop the image according to the value + */ +export class ValueBar { + // Phaser node + node: UIImage + + // Orientation + private orientation: ValueBarOrientation + + // Current value + private current = 0 + + // Maximal value + private maximal = 0 + + // Proportional value + private proportional = 0 + + // Original size + private original_width: number + private original_height: number + private crop_rect: Phaser.Geom.Rectangle + private crop_mask: Phaser.GameObjects.Graphics + + constructor(view: BaseView, name: string, orientation: ValueBarOrientation, x = 0, y = 0) { + this.node = view.newImage(name, x, y); + if (orientation == ValueBarOrientation.WEST) { + this.node.setOrigin(1, 0); + } else if (orientation == ValueBarOrientation.NORTH) { + this.node.setOrigin(0, 1); + } else { + this.node.setOrigin(0, 0); + } + + this.orientation = orientation; + this.original_width = this.node.width; + this.original_height = this.node.height; + + this.crop_rect = new Phaser.Geom.Rectangle(0, 0, this.original_width, this.original_height); + this.crop_mask = view.make.graphics({ x: x, y: y, add: false }); + this.crop_mask.fillStyle(0xffffff); + this.node.setMask(new Phaser.Display.Masks.GeometryMask(view, this.crop_mask)); + + this.setValue(0, 1000); + } + + /** + * Update the phaser graphics to match the value + */ + update() { + // TODO animation + switch (this.orientation) { + case ValueBarOrientation.EAST: + this.crop_rect.width = Math.round(this.original_width * this.proportional); + break; + case ValueBarOrientation.WEST: + this.crop_rect.width = Math.round(this.original_width * this.proportional); + this.crop_rect.x = this.original_width - this.crop_rect.width; + break; + case ValueBarOrientation.NORTH: + this.crop_rect.height = Math.round(this.original_height * this.proportional); + this.crop_rect.y = this.original_height - this.crop_rect.height; + break; + case ValueBarOrientation.SOUTH: + this.crop_rect.height = Math.round(this.original_height * this.proportional); + break; + } + + this.crop_mask.clear(); + this.crop_mask.fillRectShape(this.crop_rect); + } + + /** + * Set the current value, and maximal value + */ + setValue(current: number, maximal: number = -1) { + this.current = current > 0 ? current : 0; + if (maximal >= 0) { + this.maximal = maximal; + } + if (this.maximal === 0) { + this.proportional = 0; + } else { + this.proportional = this.current / this.maximal; + } + + this.update(); + } + + /** + * Get current raw value + */ + getValue(): number { + return this.current; + } + + /** + * Get the proportional (in 0.0-1.0 range) value + */ + getProportionalValue(): number { + return this.proportional; + } } diff --git a/src/ui/intro/IntroSteps.ts b/src/ui/intro/IntroSteps.ts index 818c6de..9c84b60 100644 --- a/src/ui/intro/IntroSteps.ts +++ b/src/ui/intro/IntroSteps.ts @@ -1,179 +1,184 @@ -module TK.SpaceTac.UI { - /** - * Sequence of steps presenting the campaign intro. - */ - export class IntroSteps { - view: IntroView - steps: Function[] = [] - current = 0 - layers: UIContainer[] = [] +import { RandomGenerator } from "../../common/RandomGenerator"; +import { range } from "../../common/Tools"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIContainer } from "../common/UIContainer"; +import { UIConversationMessage, UIConversationStyle } from "../common/UIConversation"; +import { IntroView } from "./IntroView"; - constructor(view: IntroView) { - this.view = view; - } +/** + * Sequence of steps presenting the campaign intro. + */ +export class IntroSteps { + view: IntroView + steps: Function[] = [] + current = 0 + layers: UIContainer[] = [] - /** - * Start the steps playback - */ - startPlayback() { - this.current = 0; - this.nextStep(); - } + constructor(view: IntroView) { + this.view = view; + } - /** - * Rewind the playback - */ - rewind() { - this.layers.forEach(layer => layer.removeAll(true)); - this.layers.forEach(layer => layer.destroy()); - this.layers = []; + /** + * Start the steps playback + */ + startPlayback() { + this.current = 0; + this.nextStep(); + } - this.startPlayback(); - } + /** + * Rewind the playback + */ + rewind() { + this.layers.forEach(layer => layer.removeAll(true)); + this.layers.forEach(layer => layer.destroy()); + this.layers = []; - /** - * Advance to the next step - * - * Returns false, if there are no step to display. - */ - nextStep(): boolean { - if (this.current < this.steps.length) { - let step = this.steps[this.current]; - step(); - this.current += 1; - return true; - } else { - return false; - } - } + this.startPlayback(); + } - /** - * Setup the default provided steps. - */ - setupDefaultSteps() { - this.steps = [ - this.message("In a not-so-distant future, Artifical Intelligence has become the most prominent species in the universe."), - this.message("Obsolete and outsmarted, humans have been defeated in their pitiful rebellions, and parked inside reservations."), - this.message("With the secrets of faster-than-light travel unveiled in only a handful of decades, AIs uploaded themselves in spaceships, and quickly colonized nearby galaxies."), - this.simultaneous([ - this.galaxy(), - this.message("But now, the Terranax galaxy is in turmoil."), - ]), - this.message("After centuries of unmatched peace and prosperous trading, the FTC (Federal Terranaxan Council), a group of elected representants in charge of edicting laws and organizing the Terranax Security Force, has been overtaken by forces unknown."), - this.message("No official communication has been issued since, and numerous rogue fleets have taken position in key sectors of the galaxy, forbidding passage or harassing merchants."), - this.message("The Master Merchant Guild, a powerful group that spans several galaxies, is worried about the profit loss those events incurred, and after many debates, decided to send several investigation teams to Terranax."), - this.message("Their task is to discreetly uncover the origin of the invasion, and to bring back intel that may be used by the Guild to plan an appropriate response."), - this.simultaneous( [ - this.exitftl(), - this.message("Your team has been sent through the Expeller jump system based in the Eros-MC galaxy, and just left quantum space in orbit of a Terranaxan star..."), - ]), - ]; - } - - /** - * Display a rotating galaxy - */ - protected galaxy(): Function { - return () => { - let layer = this.getLayer(0); - let animations = this.view.animations; - let mwidth = this.view.getMidWidth(); - let mheight = this.view.getMidHeight(); - - let builder = new UIBuilder(this.view, layer); - let random = RandomGenerator.global; - - let galaxy = builder.container("galaxy", 0, 0, false); - galaxy.setPosition(mwidth, mheight); - animations.show(galaxy, 3000); - animations.addAnimation(galaxy, { rotation: Math.PI * 2 }, 150000, undefined, undefined, Infinity); - - builder.in(galaxy, builder => { - let back1 = builder.image("intro-galaxy1", 0, 0, true); - back1.setScale(2.5); - let back2 = builder.image("intro-galaxy2", 0, 0, true); - back2.setScale(1.5); - animations.addAnimation(back2, { rotation: Math.PI * 2 }, 300000, undefined, undefined, Infinity); - - range(200).forEach(() => { - let distance = (0.3 + random.random() * 0.7) * mheight; - let angle = random.random() * Math.PI * 2; - let power = 0.4 + random.random() * 0.6; - - let star = builder.image("intro-star", distance * Math.cos(angle), distance * Math.sin(angle), true); - star.setScale(0.15 + random.random() * 0.2); - star.setAlpha(power * 0.5); - this.view.tweens.add({ - targets: star, - alpha: star.alpha + 0.5, - duration: 200 + random.random() * 500, - delay: 1000 + random.random() * 3000, - yoyo: true, - loop: Infinity, - loopDelay: 2000 + random.random() * 5000 - }); - }); - }); - } - } - - /** - * Display a fleet emerging from FTL - */ - protected exitftl(): Function { - return () => { - let layer = this.getLayer(1); - let builder = new UIBuilder(this.view, layer); - - let fleet = builder.image("intro-fleet", this.view.getMidWidth() + 1500, this.view.getMidHeight() - 750, true); - this.view.animations.addAnimation(fleet, { x: this.view.getMidWidth(), y: this.view.getMidHeight() }, 5000, "Circ.easeOut"); - this.view.animations.addAnimation(fleet, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, 500, "Cubic.easeOut", 3500); - - let flash = builder.image("intro-flash", this.view.getMidWidth() + 60, this.view.getMidHeight() - 30, true); - flash.setAlpha(0); - flash.setScale(0.1); - this.view.animations.addAnimation(flash, { alpha: 0.7, scaleX: 2.5, scaleY: 2.5 }, 300, "Quad.easeOut", 3500, undefined, true); - } - } - - /** - * Build a step that performs several other steps at the same time - */ - protected simultaneous(steps: Function[]): Function { - return () => { - steps.forEach(step => step()); - } - } - - /** - * Build a step to display a message. - */ - protected message(message: string, layer = 2, clear = true): Function { - return () => { - let style = new UIConversationStyle(); - style.center = true; - let parent = this.getLayer(layer, clear); - let builder = new UIBuilder(this.view, parent); - let display = new UIConversationMessage(builder, 900, 200, message, style); - display.positionRelative(0.5, 0.9); - display.setVisible(false); - display.setVisible(true, 500); - } - } - - /** - * Ensure that a layer exists, and if necessary, clean it - */ - protected getLayer(layer: number, clear = false): UIContainer { - while (this.layers.length <= layer) { - this.layers.push(this.view.getLayer(`Layer ${this.layers.length}`)); - } - - if (clear) { - this.layers[layer].removeAll(true); - } - - return this.layers[layer]; - } + /** + * Advance to the next step + * + * Returns false, if there are no step to display. + */ + nextStep(): boolean { + if (this.current < this.steps.length) { + let step = this.steps[this.current]; + step(); + this.current += 1; + return true; + } else { + return false; } + } + + /** + * Setup the default provided steps. + */ + setupDefaultSteps() { + this.steps = [ + this.message("In a not-so-distant future, Artifical Intelligence has become the most prominent species in the universe."), + this.message("Obsolete and outsmarted, humans have been defeated in their pitiful rebellions, and parked inside reservations."), + this.message("With the secrets of faster-than-light travel unveiled in only a handful of decades, AIs uploaded themselves in spaceships, and quickly colonized nearby galaxies."), + this.simultaneous([ + this.galaxy(), + this.message("But now, the Terranax galaxy is in turmoil."), + ]), + this.message("After centuries of unmatched peace and prosperous trading, the FTC (Federal Terranaxan Council), a group of elected representants in charge of edicting laws and organizing the Terranax Security Force, has been overtaken by forces unknown."), + this.message("No official communication has been issued since, and numerous rogue fleets have taken position in key sectors of the galaxy, forbidding passage or harassing merchants."), + this.message("The Master Merchant Guild, a powerful group that spans several galaxies, is worried about the profit loss those events incurred, and after many debates, decided to send several investigation teams to Terranax."), + this.message("Their task is to discreetly uncover the origin of the invasion, and to bring back intel that may be used by the Guild to plan an appropriate response."), + this.simultaneous([ + this.exitftl(), + this.message("Your team has been sent through the Expeller jump system based in the Eros-MC galaxy, and just left quantum space in orbit of a Terranaxan star..."), + ]), + ]; + } + + /** + * Display a rotating galaxy + */ + protected galaxy(): Function { + return () => { + let layer = this.getLayer(0); + let animations = this.view.animations; + let mwidth = this.view.getMidWidth(); + let mheight = this.view.getMidHeight(); + + let builder = new UIBuilder(this.view, layer); + let random = RandomGenerator.global; + + let galaxy = builder.container("galaxy", 0, 0, false); + galaxy.setPosition(mwidth, mheight); + animations.show(galaxy, 3000); + animations.addAnimation(galaxy, { rotation: Math.PI * 2 }, 150000, undefined, undefined, Infinity); + + builder.in(galaxy, builder => { + let back1 = builder.image("intro-galaxy1", 0, 0, true); + back1.setScale(2.5); + let back2 = builder.image("intro-galaxy2", 0, 0, true); + back2.setScale(1.5); + animations.addAnimation(back2, { rotation: Math.PI * 2 }, 300000, undefined, undefined, Infinity); + + range(200).forEach(() => { + let distance = (0.3 + random.random() * 0.7) * mheight; + let angle = random.random() * Math.PI * 2; + let power = 0.4 + random.random() * 0.6; + + let star = builder.image("intro-star", distance * Math.cos(angle), distance * Math.sin(angle), true); + star.setScale(0.15 + random.random() * 0.2); + star.setAlpha(power * 0.5); + this.view.tweens.add({ + targets: star, + alpha: star.alpha + 0.5, + duration: 200 + random.random() * 500, + delay: 1000 + random.random() * 3000, + yoyo: true, + loop: Infinity, + loopDelay: 2000 + random.random() * 5000 + }); + }); + }); + } + } + + /** + * Display a fleet emerging from FTL + */ + protected exitftl(): Function { + return () => { + let layer = this.getLayer(1); + let builder = new UIBuilder(this.view, layer); + + let fleet = builder.image("intro-fleet", this.view.getMidWidth() + 1500, this.view.getMidHeight() - 750, true); + this.view.animations.addAnimation(fleet, { x: this.view.getMidWidth(), y: this.view.getMidHeight() }, 5000, "Circ.easeOut"); + this.view.animations.addAnimation(fleet, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, 500, "Cubic.easeOut", 3500); + + let flash = builder.image("intro-flash", this.view.getMidWidth() + 60, this.view.getMidHeight() - 30, true); + flash.setAlpha(0); + flash.setScale(0.1); + this.view.animations.addAnimation(flash, { alpha: 0.7, scaleX: 2.5, scaleY: 2.5 }, 300, "Quad.easeOut", 3500, undefined, true); + } + } + + /** + * Build a step that performs several other steps at the same time + */ + protected simultaneous(steps: Function[]): Function { + return () => { + steps.forEach(step => step()); + } + } + + /** + * Build a step to display a message. + */ + protected message(message: string, layer = 2, clear = true): Function { + return () => { + let style = new UIConversationStyle(); + style.center = true; + let parent = this.getLayer(layer, clear); + let builder = new UIBuilder(this.view, parent); + let display = new UIConversationMessage(builder, 900, 200, message, style); + display.positionRelative(0.5, 0.9); + display.setVisible(false); + display.setVisible(true, 500); + } + } + + /** + * Ensure that a layer exists, and if necessary, clean it + */ + protected getLayer(layer: number, clear = false): UIContainer { + while (this.layers.length <= layer) { + this.layers.push(this.view.getLayer(`Layer ${this.layers.length}`)); + } + + if (clear) { + this.layers[layer].removeAll(true); + } + + return this.layers[layer]; + } } diff --git a/src/ui/intro/IntroView.ts b/src/ui/intro/IntroView.ts index 50f75c1..cff31e1 100644 --- a/src/ui/intro/IntroView.ts +++ b/src/ui/intro/IntroView.ts @@ -1,38 +1,37 @@ -/// +import { BaseView } from "../BaseView"; +import { IntroSteps } from "./IntroSteps"; -module TK.SpaceTac.UI { - /** - * View introducing the campaign story. - */ - export class IntroView extends BaseView { - create() { - super.create(); +/** + * View introducing the campaign story. + */ +export class IntroView extends BaseView { + create() { + super.create(); - let steps = new IntroSteps(this); - steps.setupDefaultSteps(); - steps.startPlayback(); + let steps = new IntroSteps(this); + steps.setupDefaultSteps(); + steps.startPlayback(); - let nextStep = () => { - if (!steps.nextStep()) { - this.session.introduced = true; - this.backToRouter(); - return false; - } else { - return true; - } - }; + let nextStep = () => { + if (!steps.nextStep()) { + this.session.introduced = true; + this.backToRouter(); + return false; + } else { + return true; + } + }; - this.input.on("pointerup", nextStep); + this.input.on("pointerup", nextStep); - this.inputs.bind("Home", "Rewind", () => steps.rewind()); - this.inputs.bind("Space", "Next step", nextStep); - this.inputs.bind("Enter", "Next step", nextStep); - this.inputs.bind("Escape", "Skip all", () => { - while (nextStep()) { - } - }); + this.inputs.bind("Home", "Rewind", () => steps.rewind()); + this.inputs.bind("Space", "Next step", nextStep); + this.inputs.bind("Enter", "Next step", nextStep); + this.inputs.bind("Escape", "Skip all", () => { + while (nextStep()) { + } + }); - this.audio.startMusic("division"); - } - } + this.audio.startMusic("division"); + } } diff --git a/src/ui/map/ActiveMissionsDisplay.spec.ts b/src/ui/map/ActiveMissionsDisplay.spec.ts index f9753fc..fff5e7c 100644 --- a/src/ui/map/ActiveMissionsDisplay.spec.ts +++ b/src/ui/map/ActiveMissionsDisplay.spec.ts @@ -1,23 +1,31 @@ -module TK.SpaceTac.UI.Specs { - testing("ActiveMissionsDisplay", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { Fleet } from "../../core/Fleet"; +import { ActiveMissions } from "../../core/missions/ActiveMissions"; +import { Mission } from "../../core/missions/Mission"; +import { MissionPart } from "../../core/missions/MissionPart"; +import { Universe } from "../../core/Universe"; +import { UIImage } from "../common/UIImage"; +import { checkText, setupEmptyView } from "../TestGame"; +import { ActiveMissionsDisplay } from "./ActiveMissionsDisplay"; - test.case("displays active missions", check => { - let view = testgame.view; - let missions = new ActiveMissions(); - let display = new ActiveMissionsDisplay(view, missions); +testing("ActiveMissionsDisplay", test => { + let testgame = setupEmptyView(test); - let container = display.container; - check.equals(container.length, 0); + test.case("displays active missions", check => { + let view = testgame.view; + let missions = new ActiveMissions(); + let display = new ActiveMissionsDisplay(view, missions); - let mission = new Mission(new Universe(), new Fleet()); - mission.addPart(new MissionPart(mission, "Get back to base")); - missions.secondary = [mission]; + let container = display.container; + check.equals(container.length, 0); - display.checkUpdate(); - check.equals(container.length, 2); - check.equals(container.list[0] instanceof UIImage, true); - checkText(check, container.list[1], "Get back to base"); - }); - }); -} + let mission = new Mission(new Universe(), new Fleet()); + mission.addPart(new MissionPart(mission, "Get back to base")); + missions.secondary = [mission]; + + display.checkUpdate(); + check.equals(container.length, 2); + check.equals(container.list[0] instanceof UIImage, true); + checkText(check, container.list[1], "Get back to base"); + }); +}); diff --git a/src/ui/map/ActiveMissionsDisplay.ts b/src/ui/map/ActiveMissionsDisplay.ts index 25093fe..54c472e 100644 --- a/src/ui/map/ActiveMissionsDisplay.ts +++ b/src/ui/map/ActiveMissionsDisplay.ts @@ -1,86 +1,90 @@ -/// +import { ActiveMissions } from "../../core/missions/ActiveMissions" +import { Star } from "../../core/Star" +import { StarLocation } from "../../core/StarLocation" +import { BaseView } from "../BaseView" +import { UIBuilder } from "../common/UIBuilder" +import { UIContainer } from "../common/UIContainer" +import { MissionLocationMarker } from "./MissionLocationMarker" -module TK.SpaceTac.UI { - /** - * Widget to display the active missions list - */ - export class ActiveMissionsDisplay { - readonly container: UIContainer - private builder: UIBuilder - private missions: ActiveMissions - private hash: number - private markers?: MissionLocationMarker +/** + * Widget to display the active missions list + */ +export class ActiveMissionsDisplay { + readonly container: UIContainer + private builder: UIBuilder + private missions: ActiveMissions + private hash: number + private markers?: MissionLocationMarker - constructor(view: BaseView, missions: ActiveMissions, markers?: MissionLocationMarker) { - let builder = new UIBuilder(view); - this.container = builder.container("active-missions"); - this.builder = builder.in(this.container); - this.missions = missions; - this.hash = missions.getHash(); - this.markers = markers; + constructor(view: BaseView, missions: ActiveMissions, markers?: MissionLocationMarker) { + let builder = new UIBuilder(view); + this.container = builder.container("active-missions"); + this.builder = builder.in(this.container); + this.missions = missions; + this.hash = missions.getHash(); + this.markers = markers; - this.update(); - } + this.update(); + } - /** - * Move to a specific location inside a parent - */ - moveTo(parent: UIContainer, x: number, y: number): void { - parent.add(this.container); - this.container.setPosition(x, y); - } + /** + * Move to a specific location inside a parent + */ + moveTo(parent: UIContainer, x: number, y: number): void { + parent.add(this.container); + this.container.setPosition(x, y); + } - setVisible(visible: boolean, duration = 0): void { - this.container.setVisible(visible, duration); - } + setVisible(visible: boolean, duration = 0): void { + this.container.setVisible(visible, duration); + } - /** - * Check if the active missions' status changed - */ - checkUpdate(): boolean { - this.missions.checkStatus(); + /** + * Check if the active missions' status changed + */ + checkUpdate(): boolean { + this.missions.checkStatus(); - let new_hash = this.missions.getHash(); - if (new_hash != this.hash) { - this.hash = new_hash; - this.update(); - return true; - } else { - return false; - } - } - - /** - * Update the current missions list - */ - private update() { - this.container.removeAll(true); - - let markers: [StarLocation | Star, string][] = []; - - let active = this.missions.getCurrent(); - let spacing = 80; - let offset = 245 - active.length * spacing; - active.forEach((mission, idx) => { - let image = mission.main ? "map-mission-main" : "map-mission-standard"; - this.builder.image(image, 35, offset + spacing * idx, true); - this.builder.text(mission.current_part.title, 90, offset + spacing * idx, { - color: "#d2e1f3", - size: 20, - width: 430, - center: false, - vcenter: true - }); - - let location = mission.current_part.getLocationHint(); - if (location) { - markers.push([location, image]); - } - }); - - if (this.markers) { - this.markers.setMarkers(markers); - } - } + let new_hash = this.missions.getHash(); + if (new_hash != this.hash) { + this.hash = new_hash; + this.update(); + return true; + } else { + return false; } -} \ No newline at end of file + } + + /** + * Update the current missions list + */ + private update() { + this.container.removeAll(true); + + let markers: [StarLocation | Star, string][] = []; + + let active = this.missions.getCurrent(); + let spacing = 80; + let offset = 245 - active.length * spacing; + active.forEach((mission, idx) => { + let image = mission.main ? "map-mission-main" : "map-mission-standard"; + this.builder.image(image, 35, offset + spacing * idx, true); + this.builder.text(mission.current_part.title, 90, offset + spacing * idx, { + color: "#d2e1f3", + size: 20, + width: 430, + center: false, + vcenter: true + }); + + let location = mission.current_part.getLocationHint(); + if (location) { + markers.push([location, image]); + } + }); + + if (this.markers) { + this.markers.setMarkers(markers); + } + } +} diff --git a/src/ui/map/CurrentLocationMarker.ts b/src/ui/map/CurrentLocationMarker.ts index 7e9ba8f..0fd2320 100644 --- a/src/ui/map/CurrentLocationMarker.ts +++ b/src/ui/map/CurrentLocationMarker.ts @@ -1,63 +1,65 @@ -module TK.SpaceTac.UI { - /** - * Marker to show current location on the map - */ - export class CurrentLocationMarker extends UIImage { - private zoom = -1; - private moving = false; - private fleet: FleetDisplay; +import { UIImage } from "../common/UIImage"; +import { FleetDisplay } from "./FleetDisplay"; +import { UniverseMapView } from "./UniverseMapView"; - constructor(private view: UniverseMapView, fleet: FleetDisplay) { - super(view, 0, 0, view.getImageInfo("map-current-location").key, view.getImageInfo("map-current-location").frame); +/** + * Marker to show current location on the map + */ +export class CurrentLocationMarker extends UIImage { + private zoom = -1; + private moving = false; + private fleet: FleetDisplay; - this.fleet = fleet; + constructor(private view: UniverseMapView, fleet: FleetDisplay) { + super(view, 0, 0, view.getImageInfo("map-current-location").key, view.getImageInfo("map-current-location").frame); - this.setOrigin(0.5, 0.5); - this.alpha = 0; - } + this.fleet = fleet; - tweenTo(alpha: number, scale: number) { - this.view.animations.addAnimation(this, { alpha: alpha, scaleX: scale, scaleY: scale }, 500); - } + this.setOrigin(0.5, 0.5); + this.alpha = 0; + } - show() { - let scale = 1; - if (this.zoom == 2) { - this.setPosition(this.fleet.x, this.fleet.y); - scale = this.fleet.scaleX * 4; - } else { - this.setPosition(this.fleet.location.star.x, this.fleet.location.star.y); - scale = (this.zoom == 1) ? 0.002 : 0.016; - } - this.setAlpha(0); - this.setScale(scale * 10); + tweenTo(alpha: number, scale: number) { + this.view.animations.addAnimation(this, { alpha: alpha, scaleX: scale, scaleY: scale }, 500); + } - this.tweenTo(1, scale); - } - - hide() { - this.tweenTo(0, this.scaleX * 10); - } - - setZoom(level: number) { - if (level != this.zoom) { - this.zoom = level; - - if (!this.moving) { - this.show(); - } - } - } - - setFleetMoving(moving: boolean) { - if (moving != this.moving) { - this.moving = moving; - if (moving) { - this.hide(); - } else { - this.show(); - } - } - } + show() { + let scale = 1; + if (this.zoom == 2) { + this.setPosition(this.fleet.x, this.fleet.y); + scale = this.fleet.scaleX * 4; + } else { + this.setPosition(this.fleet.location.star.x, this.fleet.location.star.y); + scale = (this.zoom == 1) ? 0.002 : 0.016; } -} \ No newline at end of file + this.setAlpha(0); + this.setScale(scale * 10); + + this.tweenTo(1, scale); + } + + hide() { + this.tweenTo(0, this.scaleX * 10); + } + + setZoom(level: number) { + if (level != this.zoom) { + this.zoom = level; + + if (!this.moving) { + this.show(); + } + } + } + + setFleetMoving(moving: boolean) { + if (moving != this.moving) { + this.moving = moving; + if (moving) { + this.hide(); + } else { + this.show(); + } + } + } +} diff --git a/src/ui/map/FleetDisplay.spec.ts b/src/ui/map/FleetDisplay.spec.ts index 6c306c5..e50e25f 100644 --- a/src/ui/map/FleetDisplay.spec.ts +++ b/src/ui/map/FleetDisplay.spec.ts @@ -1,43 +1,47 @@ -module TK.SpaceTac.UI.Specs { - testing("FleetDisplay", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { first, nn } from "../../common/Tools"; +import { GameSession } from "../../core/GameSession"; +import { checkTween, setupEmptyView } from "../TestGame"; +import { FleetDisplay } from "./FleetDisplay"; - test.case("orbits the fleet around its current location", check => { - let session = new GameSession(); - session.startNewGame(true, false); - let fleet = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); +testing("FleetDisplay", test => { + let testgame = setupEmptyView(test); - fleet.loopOrbit(); - check.equals(fleet.rotation, 0); + test.case("orbits the fleet around its current location", check => { + let session = new GameSession(); + session.startNewGame(true, false); + let fleet = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); - checkTween(testgame, fleet, "rotation", [0, -Math.PI * 2 / 3, Math.PI * 2 / 3, 0]); - }); + fleet.loopOrbit(); + check.equals(fleet.rotation, 0); - test.case("animates jumps between locations", check => { - let session = new GameSession(); - session.startNewGame(true, false); - let fleet_disp = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); + checkTween(testgame, fleet, "rotation", [0, -Math.PI * 2 / 3, Math.PI * 2 / 3, 0]); + }); - let on_leave = check.mockfunc("on_leave", (duration: number): any => null); - let on_finished = check.mockfunc(); + test.case("animates jumps between locations", check => { + let session = new GameSession(); + session.startNewGame(true, false); + let fleet_disp = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); - let current = nn(session.universe.getLocation(session.player.fleet.location)); - let dest = nn(first(current.star.locations, loc => loc !== current)); - dest.universe_x = current.universe_x - 0.1; - dest.universe_y = current.universe_y; - dest.clearEncounter(); - fleet_disp.moveToLocation(dest, 1, on_leave.func, on_finished.func); - check.called(on_leave, 0); - check.called(on_finished, 0); - checkTween(testgame, fleet_disp, "rotation", [ - 0, - -0.0436332312998573, - -0.3490658503988655, - -1.178097245096172, - -2.7925268031909276, - 0.8290313946973056, - -3.141592653589793 - ]); - }); - }); -} + let on_leave = check.mockfunc("on_leave", (duration: number): any => null); + let on_finished = check.mockfunc(); + + let current = nn(session.universe.getLocation(session.player.fleet.location)); + let dest = nn(first(current.star.locations, loc => loc !== current)); + dest.universe_x = current.universe_x - 0.1; + dest.universe_y = current.universe_y; + dest.clearEncounter(); + fleet_disp.moveToLocation(dest, 1, on_leave.func, on_finished.func); + check.called(on_leave, 0); + check.called(on_finished, 0); + checkTween(testgame, fleet_disp, "rotation", [ + 0, + -0.0436332312998573, + -0.3490658503988655, + -1.178097245096172, + -2.7925268031909276, + 0.8290313946973056, + -3.141592653589793 + ]); + }); +}); diff --git a/src/ui/map/FleetDisplay.ts b/src/ui/map/FleetDisplay.ts index 1257bd4..a7ce752 100644 --- a/src/ui/map/FleetDisplay.ts +++ b/src/ui/map/FleetDisplay.ts @@ -1,143 +1,149 @@ -module TK.SpaceTac.UI { - const SCALING = 0.00005; - const LOCATIONS: [number, number][] = [ - [80, 0], - [0, -50], - [0, 50], - [-80, 0], - [0, 0], - ]; - const PI2 = Math.PI * 2; +import { Fleet } from "../../core/Fleet"; +import { StarLocation } from "../../core/StarLocation"; +import { Universe } from "../../core/Universe"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIContainer } from "../common/UIContainer"; +import { CurrentLocationMarker } from "./CurrentLocationMarker"; - /** - * Group to display a fleet - */ - export class FleetDisplay extends UIContainer { - private ship_count = 0 - private is_moving = false +const SCALING = 0.00005; +const LOCATIONS: [number, number][] = [ + [80, 0], + [0, -50], + [0, 50], + [-80, 0], + [0, 0], +]; +const PI2 = Math.PI * 2; - constructor(private map: BaseView, private fleet: Fleet, private universe: Universe, private location_marker?: CurrentLocationMarker, orbit = true) { - super(map); +/** + * Group to display a fleet + */ +export class FleetDisplay extends UIContainer { + private ship_count = 0 + private is_moving = false - this.updateShipSprites(); + constructor(private map: BaseView, private fleet: Fleet, private universe: Universe, private location_marker?: CurrentLocationMarker, orbit = true) { + super(map); - let location = this.universe.getLocation(fleet.location); - if (location) { - this.setPosition(location.star.x + location.x, location.star.y + location.y); - } - this.setScale(SCALING, SCALING); + this.updateShipSprites(); - if (orbit) { - this.loopOrbit(); - } - } - - /** - * Update the ship sprites - */ - updateShipSprites() { - if (this.ship_count != this.fleet.ships.length) { - let builder = new UIBuilder(this.map, this); - - builder.clear(); - - this.fleet.ships.forEach((ship, index) => { - let offset = LOCATIONS[index]; - let sprite = builder.image(`ship-${ship.model.code}-sprite`, offset[0], offset[1] + 150, true); - sprite.setScale(64 / sprite.width); - }); - - this.ship_count = this.fleet.ships.length; - } - } - - get location(): StarLocation { - return this.universe.getLocation(this.fleet.location) || new StarLocation(); - } - - /** - * Animate to a given position in orbit of its current star location - */ - async goToOrbitPoint(angle: number, speed = 1, fullturns = 0, ease = false): Promise { - this.map.animations.killPrevious(this, ["angle"]); - this.rotation %= PI2; - - let target = -angle; - while (target >= this.rotation) { - target -= PI2; - } - target -= PI2 * fullturns; - let distance = Math.abs(target - this.rotation) / PI2; - await this.map.animations.addAnimation(this, { rotation: target }, 30000 * distance / speed, ease ? "Cubic.easeIn" : "Linear"); - } - - /** - * Make the fleet loop in orbit - */ - loopOrbit() { - if (!this.is_moving) { - this.goToOrbitPoint(this.rotation + PI2, 1, 0).then(() => this.loopOrbit()); - } - } - - /** - * Make the fleet move to another location in the same system - */ - moveToLocation(location: StarLocation, speed = 1, on_leave: ((duration: number) => any) | null = null, on_finished: Function | null = null) { - let fleet_location = this.universe.getLocation(this.fleet.location); - if (fleet_location && this.fleet.move(location)) { - let dx = location.universe_x - fleet_location.universe_x; - let dy = location.universe_y - fleet_location.universe_y; - let distance = Math.sqrt(dx * dx + dy * dy); - let angle = Math.atan2(-dy, dx); - this.setMoving(true); - this.goToOrbitPoint(angle, 40, 1, true).then(() => { - this.setRotation(-angle); - let duration = 10000 * distance / speed; - if (on_leave) { - on_leave(duration); - } - let tween = this.map.animations.addAnimation(this, { x: this.x + dx, y: this.y + dy }, duration, "Cubic.easeOut"); - tween.then(() => { - if (this.fleet.battle) { - this.map.backToRouter(); - } else { - this.setMoving(false); - this.loopOrbit(); - } - - if (on_finished) { - on_finished(); - } - }); - }); - } - } - - /** - * Display a jump flash effect - */ - async showJumpEffect(lag = 0, duration = 0): Promise { - this.map.audio.playOnce(lag ? "map-warp-out" : "map-warp-in"); - let effect = this.getBuilder().image("map-jump-effect", 0, 150, true); - effect.setScale(0.01); - effect.setZ(-1); - if (lag && duration) { - this.map.animations.addAnimation(effect, { x: -lag / SCALING }, duration * 0.5, "Cubic.easeOut"); - } - await this.map.animations.addAnimation(effect, { scaleX: 3, scaleY: 3 }, 100); - await this.map.animations.addAnimation(effect, { scaleX: 2, scaleY: 2, alpha: 0 }, 200); - effect.destroy(); - } - - /** - * Mark the fleet as moving - */ - private setMoving(moving: boolean): void { - this.is_moving = moving; - if (this.location_marker) { - this.location_marker.setFleetMoving(moving); - } - } + let location = this.universe.getLocation(fleet.location); + if (location) { + this.setPosition(location.star.x + location.x, location.star.y + location.y); } -} \ No newline at end of file + this.setScale(SCALING, SCALING); + + if (orbit) { + this.loopOrbit(); + } + } + + /** + * Update the ship sprites + */ + updateShipSprites() { + if (this.ship_count != this.fleet.ships.length) { + let builder = new UIBuilder(this.map, this); + + builder.clear(); + + this.fleet.ships.forEach((ship, index) => { + let offset = LOCATIONS[index]; + let sprite = builder.image(`ship-${ship.model.code}-sprite`, offset[0], offset[1] + 150, true); + sprite.setScale(64 / sprite.width); + }); + + this.ship_count = this.fleet.ships.length; + } + } + + get location(): StarLocation { + return this.universe.getLocation(this.fleet.location) || new StarLocation(); + } + + /** + * Animate to a given position in orbit of its current star location + */ + async goToOrbitPoint(angle: number, speed = 1, fullturns = 0, ease = false): Promise { + this.map.animations.killPrevious(this, ["angle"]); + this.rotation %= PI2; + + let target = -angle; + while (target >= this.rotation) { + target -= PI2; + } + target -= PI2 * fullturns; + let distance = Math.abs(target - this.rotation) / PI2; + await this.map.animations.addAnimation(this, { rotation: target }, 30000 * distance / speed, ease ? "Cubic.easeIn" : "Linear"); + } + + /** + * Make the fleet loop in orbit + */ + loopOrbit() { + if (!this.is_moving) { + this.goToOrbitPoint(this.rotation + PI2, 1, 0).then(() => this.loopOrbit()); + } + } + + /** + * Make the fleet move to another location in the same system + */ + moveToLocation(location: StarLocation, speed = 1, on_leave: ((duration: number) => any) | null = null, on_finished: Function | null = null) { + let fleet_location = this.universe.getLocation(this.fleet.location); + if (fleet_location && this.fleet.move(location)) { + let dx = location.universe_x - fleet_location.universe_x; + let dy = location.universe_y - fleet_location.universe_y; + let distance = Math.sqrt(dx * dx + dy * dy); + let angle = Math.atan2(-dy, dx); + this.setMoving(true); + this.goToOrbitPoint(angle, 40, 1, true).then(() => { + this.setRotation(-angle); + let duration = 10000 * distance / speed; + if (on_leave) { + on_leave(duration); + } + let tween = this.map.animations.addAnimation(this, { x: this.x + dx, y: this.y + dy }, duration, "Cubic.easeOut"); + tween.then(() => { + if (this.fleet.battle) { + this.map.backToRouter(); + } else { + this.setMoving(false); + this.loopOrbit(); + } + + if (on_finished) { + on_finished(); + } + }); + }); + } + } + + /** + * Display a jump flash effect + */ + async showJumpEffect(lag = 0, duration = 0): Promise { + this.map.audio.playOnce(lag ? "map-warp-out" : "map-warp-in"); + let effect = this.getBuilder().image("map-jump-effect", 0, 150, true); + effect.setScale(0.01); + effect.setZ(-1); + if (lag && duration) { + this.map.animations.addAnimation(effect, { x: -lag / SCALING }, duration * 0.5, "Cubic.easeOut"); + } + await this.map.animations.addAnimation(effect, { scaleX: 3, scaleY: 3 }, 100); + await this.map.animations.addAnimation(effect, { scaleX: 2, scaleY: 2, alpha: 0 }, 200); + effect.destroy(); + } + + /** + * Mark the fleet as moving + */ + private setMoving(moving: boolean): void { + this.is_moving = moving; + if (this.location_marker) { + this.location_marker.setFleetMoving(moving); + } + } +} diff --git a/src/ui/map/MapLocationMenu.ts b/src/ui/map/MapLocationMenu.ts index 975c6fb..7e9020b 100644 --- a/src/ui/map/MapLocationMenu.ts +++ b/src/ui/map/MapLocationMenu.ts @@ -1,70 +1,74 @@ -module TK.SpaceTac.UI { - /** - * Menu to display selected map location, and associated actions - */ - export class MapLocationMenu { - readonly container: UIContainer - private content: UIBuilder +import { StarLocation, StarLocationType } from "../../core/StarLocation"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIContainer } from "../common/UIContainer"; +import { UniverseMapView } from "./UniverseMapView"; - constructor(private view: BaseView, parent?: UIContainer, x = 0, y = 0) { - let builder = new UIBuilder(view, parent); - this.container = builder.container("location-menu", x, y); - this.content = builder.in(this.container); - } +/** + * Menu to display selected map location, and associated actions + */ +export class MapLocationMenu { + readonly container: UIContainer + private content: UIBuilder - /** - * Set information displayed, with title and actions to show in menu - */ - setInfo(title: string, actions: [string, Function][]) { - this.content.clear(); + constructor(private view: BaseView, parent?: UIContainer, x = 0, y = 0) { + let builder = new UIBuilder(view, parent); + this.container = builder.container("location-menu", x, y); + this.content = builder.in(this.container); + } - if (title) { - this.content.image("map-subname", 239, 57, true); - this.content.text(title, 239, 57, { color: "#b8d2f1", size: 22 }) - } + /** + * Set information displayed, with title and actions to show in menu + */ + setInfo(title: string, actions: [string, Function][]) { + this.content.clear(); - for (let idx = actions.length - 1; idx >= 0; idx--) { - let [label, action] = actions[idx]; - this.content.button("map-action", 172, 48 + idx * 100 + 96, action, undefined, undefined, { center: true }); - this.content.text(label, 186, 48 + idx * 100 + 136, { color: "#b8d2f1", size: 20 }); - } - } - - /** - * Automatically set menu content from current location - */ - setFromLocation(location: StarLocation | null, view: UniverseMapView) { - if (location) { - let actions: [string, Function][] = []; - if (location.shop) { - let shop = location.shop; - actions.push(["Go to dockyard", () => view.openShop()]); - actions.push(["Show jobs", () => view.openMissions()]); - } - - switch (location.type) { - case StarLocationType.WARP: - this.setInfo("Warp-zone", actions.concat([["Engage jump drive", () => view.doJump()]])); - break; - case StarLocationType.STAR: - this.setInfo("Class II Star", actions); - break; - case StarLocationType.PLANET: - this.setInfo("Rock planet", actions); - break; - case StarLocationType.ASTEROID: - this.setInfo("Huge asteroid", actions); - break; - case StarLocationType.STATION: - this.setInfo("Space station", actions); - break; - default: - this.setInfo("Somewhere in space", actions); - break; - } - } else { - this.setInfo("", []); - } - } + if (title) { + this.content.image("map-subname", 239, 57, true); + this.content.text(title, 239, 57, { color: "#b8d2f1", size: 22 }) } -} \ No newline at end of file + + for (let idx = actions.length - 1; idx >= 0; idx--) { + let [label, action] = actions[idx]; + this.content.button("map-action", 172, 48 + idx * 100 + 96, action, undefined, undefined, { center: true }); + this.content.text(label, 186, 48 + idx * 100 + 136, { color: "#b8d2f1", size: 20 }); + } + } + + /** + * Automatically set menu content from current location + */ + setFromLocation(location: StarLocation | null, view: UniverseMapView) { + if (location) { + let actions: [string, Function][] = []; + if (location.shop) { + let shop = location.shop; + actions.push(["Go to dockyard", () => view.openShop()]); + actions.push(["Show jobs", () => view.openMissions()]); + } + + switch (location.type) { + case StarLocationType.WARP: + this.setInfo("Warp-zone", actions.concat([["Engage jump drive", () => view.doJump()]])); + break; + case StarLocationType.STAR: + this.setInfo("Class II Star", actions); + break; + case StarLocationType.PLANET: + this.setInfo("Rock planet", actions); + break; + case StarLocationType.ASTEROID: + this.setInfo("Huge asteroid", actions); + break; + case StarLocationType.STATION: + this.setInfo("Space station", actions); + break; + default: + this.setInfo("Somewhere in space", actions); + break; + } + } else { + this.setInfo("", []); + } + } +} diff --git a/src/ui/map/MissionConversationDisplay.ts b/src/ui/map/MissionConversationDisplay.ts index 5aee3c9..43e667b 100644 --- a/src/ui/map/MissionConversationDisplay.ts +++ b/src/ui/map/MissionConversationDisplay.ts @@ -1,74 +1,77 @@ -/// +import { first } from "../../common/Tools"; +import { ActiveMissions } from "../../core/missions/ActiveMissions"; +import { MissionPartConversation } from "../../core/missions/MissionPartConversation"; +import { Ship } from "../../core/Ship"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIConversation } from "../common/UIConversation"; -module TK.SpaceTac.UI { - /** - * Display of an active mission conversation - */ - export class MissionConversationDisplay extends UIConversation { - dialog: MissionPartConversation | null = null - on_ended: Function | null = null +/** + * Display of an active mission conversation + */ +export class MissionConversationDisplay extends UIConversation { + dialog: MissionPartConversation | null = null + on_ended: Function | null = null - constructor(builder: UIBuilder) { - super(builder, () => true); - } + constructor(builder: UIBuilder) { + super(builder, () => true); + } - /** - * Update from currently active missions - */ - updateFromMissions(missions: ActiveMissions, on_ended: Function | null = null) { - let parts = missions.getCurrent().map(mission => mission.current_part); - this.dialog = first(parts, part => part instanceof MissionPartConversation); + /** + * Update from currently active missions + */ + updateFromMissions(missions: ActiveMissions, on_ended: Function | null = null) { + let parts = missions.getCurrent().map(mission => mission.current_part); + this.dialog = first(parts, part => part instanceof MissionPartConversation); - if (this.dialog) { - this.on_ended = on_ended; - } else { - this.on_ended = null; - } - - this.refresh(); - } - - /** - * Go to the next dialog piece - */ - forward(): void { - if (this.dialog) { - this.dialog.next(); - this.refresh(); - } - } - - /** - * Skip the conversation - */ - skipConversation(): void { - if (this.dialog) { - this.dialog.skip(); - this.refresh(); - } - } - - /** - * Refresh the displayed dialog piece - */ - refresh() { - this.clearContent(); - - if (this.dialog) { - if (this.dialog.checkCompleted()) { - if (this.on_ended) { - this.on_ended(); - this.on_ended = null; - } - this.setVisible(false, 700); - } else { - let piece = this.dialog.getCurrent(); - this.setCurrentShipMessage(piece.interlocutor || new Ship(), piece.message); - this.setVisible(true, 700); - } - } else { - this.setVisible(false); - } - } + if (this.dialog) { + this.on_ended = on_ended; + } else { + this.on_ended = null; } -} \ No newline at end of file + + this.refresh(); + } + + /** + * Go to the next dialog piece + */ + forward(): void { + if (this.dialog) { + this.dialog.next(); + this.refresh(); + } + } + + /** + * Skip the conversation + */ + skipConversation(): void { + if (this.dialog) { + this.dialog.skip(); + this.refresh(); + } + } + + /** + * Refresh the displayed dialog piece + */ + refresh() { + this.clearContent(); + + if (this.dialog) { + if (this.dialog.checkCompleted()) { + if (this.on_ended) { + this.on_ended(); + this.on_ended = null; + } + this.setVisible(false, 700); + } else { + let piece = this.dialog.getCurrent(); + this.setCurrentShipMessage(piece.interlocutor || new Ship(), piece.message); + this.setVisible(true, 700); + } + } else { + this.setVisible(false); + } + } +} diff --git a/src/ui/map/MissionLocationMarker.ts b/src/ui/map/MissionLocationMarker.ts index 06e14b8..d98a634 100644 --- a/src/ui/map/MissionLocationMarker.ts +++ b/src/ui/map/MissionLocationMarker.ts @@ -1,69 +1,73 @@ -module TK.SpaceTac.UI { - /** - * Marker to show a mission location on the map - */ - export class MissionLocationMarker { - private builder: UIBuilder - private markers: [StarLocation | Star, string][] = [] - private zoomed = true - private current_star?: Star +import { Star } from "../../core/Star"; +import { StarLocation } from "../../core/StarLocation"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIContainer } from "../common/UIContainer"; - constructor(private view: BaseView, parent: UIContainer) { - this.view = view; +/** + * Marker to show a mission location on the map + */ +export class MissionLocationMarker { + private builder: UIBuilder + private markers: [StarLocation | Star, string][] = [] + private zoomed = true + private current_star?: Star - let builder = new UIBuilder(view, parent); - this.builder = builder.in(builder.container("mission_markers")); - } + constructor(private view: BaseView, parent: UIContainer) { + this.view = view; - /** - * Set the active markers (location and image name) - */ - setMarkers(markers: [StarLocation | Star, string][]): void { - this.markers = markers; - this.refresh(); - } + let builder = new UIBuilder(view, parent); + this.builder = builder.in(builder.container("mission_markers")); + } - /** - * Set the zoom level - */ - setZoom(level: number, star: Star): void { - this.zoomed = level >= 2; - this.current_star = star; - this.refresh(); - } + /** + * Set the active markers (location and image name) + */ + setMarkers(markers: [StarLocation | Star, string][]): void { + this.markers = markers; + this.refresh(); + } - /** - * Refresh the display - */ - refresh(): void { - this.builder.clear(); + /** + * Set the zoom level + */ + setZoom(level: number, star: Star): void { + this.zoomed = level >= 2; + this.current_star = star; + this.refresh(); + } - this.markers.forEach(([location, name], index) => { - let focus = this.zoomed ? location : (location instanceof StarLocation ? location.star : location); - if (location !== this.current_star || !this.zoomed) { - let marker = this.getMarker(focus, index - 1); - let image = this.builder.image(name, marker.x, marker.y); - image.setScale(marker.scale); - } - }); - } + /** + * Refresh the display + */ + refresh(): void { + this.builder.clear(); - private getMarker(focus: Star | StarLocation, offset: number): { x: number, y: number, scale: number } { - if (focus instanceof StarLocation) { - let system = focus.star; - return { - x: focus.universe_x + offset * system.radius * 0.05, - y: focus.universe_y - system.radius * 0.08, - scale: system.radius * 0.001 - } - } else { - return { - x: focus.x + offset * focus.radius * 0.6, - y: focus.y - focus.radius * 0.7, - scale: focus.radius * 0.01 - } - } + this.markers.forEach(([location, name], index) => { + let focus = this.zoomed ? location : (location instanceof StarLocation ? location.star : location); + if (location !== this.current_star || !this.zoomed) { + let marker = this.getMarker(focus, index - 1); + let image = this.builder.image(name, marker.x, marker.y); + image.setScale(marker.scale); + } + }); + } - } + private getMarker(focus: Star | StarLocation, offset: number): { x: number, y: number, scale: number } { + if (focus instanceof StarLocation) { + let system = focus.star; + return { + x: focus.universe_x + offset * system.radius * 0.05, + y: focus.universe_y - system.radius * 0.08, + scale: system.radius * 0.001 + } + } else { + return { + x: focus.x + offset * focus.radius * 0.6, + y: focus.y - focus.radius * 0.7, + scale: focus.radius * 0.01 + } } -} \ No newline at end of file + + } +} diff --git a/src/ui/map/MissionsDialog.spec.ts b/src/ui/map/MissionsDialog.spec.ts index 8fda5cd..196e823 100644 --- a/src/ui/map/MissionsDialog.spec.ts +++ b/src/ui/map/MissionsDialog.spec.ts @@ -1,52 +1,58 @@ -module TK.SpaceTac.UI.Specs { - testing("MissionsDialog", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { Mission, MissionDifficulty } from "../../core/missions/Mission"; +import { Player } from "../../core/Player"; +import { Shop } from "../../core/Shop"; +import { Universe } from "../../core/Universe"; +import { collectTexts, setupEmptyView } from "../TestGame"; +import { MissionsDialog } from "./MissionsDialog"; - test.case("displays active and proposed missions", check => { - let universe = new Universe(); - let player = new Player(); - let shop = new Shop(); - let shop_missions: Mission[] = []; - check.patch(shop, "getMissions", () => shop_missions); +testing("MissionsDialog", test => { + let testgame = setupEmptyView(test); - function checkTexts(dialog: MissionsDialog, expected: string[]) { - check.equals(collectTexts(dialog.base), expected); - } + test.case("displays active and proposed missions", check => { + let universe = new Universe(); + let player = new Player(); + let shop = new Shop(); + let shop_missions: Mission[] = []; + check.patch(shop, "getMissions", () => shop_missions); - let missions = new MissionsDialog(testgame.view, shop, player); - checkTexts(missions, []); + function checkTexts(dialog: MissionsDialog, expected: string[]) { + check.equals(collectTexts(dialog.base), expected); + } - let mission = new Mission(universe); - mission.title = "Save the universe!"; - mission.setDifficulty(MissionDifficulty.hard, 1); - mission.reward = 15000; - shop_missions.push(mission); - missions.refresh(); - checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys"]); + let missions = new MissionsDialog(testgame.view, shop, player); + checkTexts(missions, []); - mission = new Mission(universe); - mission.title = "Do not do evil"; - mission.setDifficulty(MissionDifficulty.easy, 1); - mission.reward = 3500; - shop_missions.push(mission); - missions.refresh(); - checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); + let mission = new Mission(universe); + mission.title = "Save the universe!"; + mission.setDifficulty(MissionDifficulty.hard, 1); + mission.reward = 15000; + shop_missions.push(mission); + missions.refresh(); + checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys"]); - mission = new Mission(universe); - mission.title = "Collect some money"; - mission.setDifficulty(MissionDifficulty.normal, 1); - player.missions.addSecondary(mission, player.fleet); - missions.refresh(); - checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", - "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); + mission = new Mission(universe); + mission.title = "Do not do evil"; + mission.setDifficulty(MissionDifficulty.easy, 1); + mission.reward = 3500; + shop_missions.push(mission); + missions.refresh(); + checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); - mission = new Mission(universe, undefined, true); - mission.title = "Kill the villain"; - mission.setDifficulty(MissionDifficulty.hard, 1); - player.missions.main = mission; - missions.refresh(); - checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", - "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); - }); - }); -} + mission = new Mission(universe); + mission.title = "Collect some money"; + mission.setDifficulty(MissionDifficulty.normal, 1); + player.missions.addSecondary(mission, player.fleet); + missions.refresh(); + checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); + + mission = new Mission(universe, undefined, true); + mission.title = "Kill the villain"; + mission.setDifficulty(MissionDifficulty.hard, 1); + player.missions.main = mission; + missions.refresh(); + checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); + }); +}); diff --git a/src/ui/map/MissionsDialog.ts b/src/ui/map/MissionsDialog.ts index 93baaed..b9f4d8c 100644 --- a/src/ui/map/MissionsDialog.ts +++ b/src/ui/map/MissionsDialog.ts @@ -1,75 +1,81 @@ -module TK.SpaceTac.UI { - /** - * Dialog to show available missions - */ - export class MissionsDialog extends UIDialog { - shop: Shop - player: Player - location: StarLocation - on_change: Function +import { capitalize } from "../../common/Tools"; +import { Mission, MissionDifficulty } from "../../core/missions/Mission"; +import { Player } from "../../core/Player"; +import { Shop } from "../../core/Shop"; +import { StarLocation } from "../../core/StarLocation"; +import { BaseView } from "../BaseView"; +import { UIDialog } from "../common/UIDialog"; - constructor(view: BaseView, shop: Shop, player: Player, on_change?: Function) { - super(view); +/** + * Dialog to show available missions + */ +export class MissionsDialog extends UIDialog { + shop: Shop + player: Player + location: StarLocation + on_change: Function - this.shop = shop; - this.player = player; - this.location = view.session.getLocation(); - this.on_change = on_change || (() => null); + constructor(view: BaseView, shop: Shop, player: Player, on_change?: Function) { + super(view); - this.addCloseButton(); - this.refresh(); - } + this.shop = shop; + this.player = player; + this.location = view.session.getLocation(); + this.on_change = on_change || (() => null); - /** - * Refresh the dialog content - */ - refresh() { - this.content.clear(); + this.addCloseButton(); + this.refresh(); + } - let offset = 160; + /** + * Refresh the dialog content + */ + refresh() { + this.content.clear(); - let active = this.player.missions.getCurrent().filter(mission => !mission.main); - if (active.length) { - this.content.text("Active jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 }); - offset += 110; + let offset = 160; - active.forEach(mission => { - this.addMission(offset, mission, true, () => null); - offset += 110; - }); - } + let active = this.player.missions.getCurrent().filter(mission => !mission.main); + if (active.length) { + this.content.text("Active jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 }); + offset += 110; - let proposed = this.shop.getMissions(this.location); - if (proposed.length) { - this.content.text("Proposed jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 }); - offset += 110; - - proposed.forEach(mission => { - this.addMission(offset, mission, false, () => { - this.shop.acceptMission(mission, this.player); - this.close(); - this.on_change(); - }); - offset += 110; - }); - } - } - - /** - * Add a mission text - */ - addMission(yoffset: number, mission: Mission, active: boolean, button_callback: Function) { - let title = mission.title; - let subtitle = `${capitalize(MissionDifficulty[mission.difficulty])} - Reward: ${mission.getRewardText()}`; - - this.content.image("map-mission-standard", 320, yoffset, true); - if (title) { - this.content.text(title, 380, yoffset - 15, { color: "#d2e1f3", size: 22, width: 620, center: false }); - } - if (subtitle) { - this.content.text(subtitle, 380, yoffset + 22, { color: "#d2e1f3", size: 18, width: 620, center: false }); - } - this.content.button(active ? "map-mission-action-cancel" : "map-mission-action-accept", 1120, yoffset, button_callback, undefined, undefined, { center: true }); - } + active.forEach(mission => { + this.addMission(offset, mission, true, () => null); + offset += 110; + }); } -} \ No newline at end of file + + let proposed = this.shop.getMissions(this.location); + if (proposed.length) { + this.content.text("Proposed jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 }); + offset += 110; + + proposed.forEach(mission => { + this.addMission(offset, mission, false, () => { + this.shop.acceptMission(mission, this.player); + this.close(); + this.on_change(); + }); + offset += 110; + }); + } + } + + /** + * Add a mission text + */ + addMission(yoffset: number, mission: Mission, active: boolean, button_callback: Function) { + let title = mission.title; + let subtitle = `${capitalize(MissionDifficulty[mission.difficulty])} - Reward: ${mission.getRewardText()}`; + + this.content.image("map-mission-standard", 320, yoffset, true); + if (title) { + this.content.text(title, 380, yoffset - 15, { color: "#d2e1f3", size: 22, width: 620, center: false }); + } + if (subtitle) { + this.content.text(subtitle, 380, yoffset + 22, { color: "#d2e1f3", size: 18, width: 620, center: false }); + } + this.content.button(active ? "map-mission-action-cancel" : "map-mission-action-accept", 1120, yoffset, button_callback, undefined, undefined, { center: true }); + } +} diff --git a/src/ui/map/StarSystemDisplay.spec.ts b/src/ui/map/StarSystemDisplay.spec.ts index 39a82e1..2a4bb6a 100644 --- a/src/ui/map/StarSystemDisplay.spec.ts +++ b/src/ui/map/StarSystemDisplay.spec.ts @@ -1,32 +1,35 @@ -module TK.SpaceTac.UI.Specs { - testing("StarSystemDisplay", test => { - let testgame = setupMapview(test); +import { testing } from "../../common/Testing"; +import { first, nn } from "../../common/Tools"; +import { Shop } from "../../core/Shop"; +import { setupMapview } from "../TestGame"; - test.case("displays a badge with the current state for a star location", check => { - let mapview = testgame.view; - let location = mapview.player_fleet.location; +testing("StarSystemDisplay", test => { + let testgame = setupMapview(test); - let ssdisplay = nn(first(mapview.starsystems, ss => ss.starsystem == location.star)); + test.case("displays a badge with the current state for a star location", check => { + let mapview = testgame.view; + let location = mapview.player_fleet.location; - let ldisplay = nn(first(ssdisplay.locations, loc => loc[0] != location)); - check.equals(ldisplay[2].name, "map-status-unvisited"); + let ssdisplay = nn(first(mapview.starsystems, ss => ss.starsystem == location.star)); - ldisplay[0].setupEncounter(); - ssdisplay.updateInfo(2, true); - check.equals(ldisplay[2].name, "map-status-unvisited"); + let ldisplay = nn(first(ssdisplay.locations, loc => loc[0] != location)); + check.equals(ldisplay[2].name, "map-status-unvisited"); - mapview.player.fleet.visited.push(ldisplay[0].id); - ssdisplay.updateInfo(2, true); - check.equals(ldisplay[2].name, "map-status-enemy"); + ldisplay[0].setupEncounter(); + ssdisplay.updateInfo(2, true); + check.equals(ldisplay[2].name, "map-status-unvisited"); - ldisplay[0].shop = null; - ldisplay[0].clearEncounter(); - ssdisplay.updateInfo(2, true); - check.equals(ldisplay[2].name, "map-status-clear"); + mapview.player.fleet.visited.push(ldisplay[0].id); + ssdisplay.updateInfo(2, true); + check.equals(ldisplay[2].name, "map-status-enemy"); - ldisplay[0].shop = new Shop(); - ssdisplay.updateInfo(2, true); - check.equals(ldisplay[2].name, "map-status-dockyard"); - }); - }); -} + ldisplay[0].shop = null; + ldisplay[0].clearEncounter(); + ssdisplay.updateInfo(2, true); + check.equals(ldisplay[2].name, "map-status-clear"); + + ldisplay[0].shop = new Shop(); + ssdisplay.updateInfo(2, true); + check.equals(ldisplay[2].name, "map-status-dockyard"); + }); +}); diff --git a/src/ui/map/StarSystemDisplay.ts b/src/ui/map/StarSystemDisplay.ts index ae374bb..97fc67d 100644 --- a/src/ui/map/StarSystemDisplay.ts +++ b/src/ui/map/StarSystemDisplay.ts @@ -1,131 +1,139 @@ -module TK.SpaceTac.UI { - /** - * Group to display a star system - */ - export class StarSystemDisplay extends UIContainer { - view: UniverseMapView - builder: UIBuilder - background: UIImage - circles: UIContainer - starsystem: Star - player: Player - fleet_display: FleetDisplay - locations: [StarLocation, UIImage | UIButton, UIImage][] = [] - label: UIButton +import { Player } from "../../core/Player" +import { Star } from "../../core/Star" +import { StarLocation, StarLocationType } from "../../core/StarLocation" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { UIImage } from "../common/UIImage" +import { FleetDisplay } from "./FleetDisplay" +import { UniverseMapView } from "./UniverseMapView" - constructor(parent: UniverseMapView, starsystem: Star) { - super(parent, starsystem.x, starsystem.y); +/** + * Group to display a star system + */ +export class StarSystemDisplay extends UIContainer { + view: UniverseMapView + builder: UIBuilder + background: UIImage + circles: UIContainer + starsystem: Star + player: Player + fleet_display: FleetDisplay + locations: [StarLocation, UIImage | UIButton, UIImage][] = [] + label: UIButton - this.view = parent; - this.builder = new UIBuilder(parent, this); + constructor(parent: UniverseMapView, starsystem: Star) { + super(parent, starsystem.x, starsystem.y); - this.background = this.builder.image("map-starsystem-background", 0, 0, true); - this.setScale(starsystem.radius * 2 / this.background.width); + this.view = parent; + this.builder = new UIBuilder(parent, this); - this.starsystem = starsystem; - this.player = parent.player; - this.fleet_display = parent.player_fleet; + this.background = this.builder.image("map-starsystem-background", 0, 0, true); + this.setScale(starsystem.radius * 2 / this.background.width); - // Show boundary - this.circles = this.builder.container("circles"); - let boundaries = this.builder.in(this.circles).image("map-boundaries", 0, 0, true); - boundaries.setScale(starsystem.radius / (this.scaleX * 256)); + this.starsystem = starsystem; + this.player = parent.player; + this.fleet_display = parent.player_fleet; - // Show locations - starsystem.locations.map(location => { - let location_sprite: UIImage | UIButton | null = null; - let loctype = StarLocationType[location.type].toLowerCase(); + // Show boundary + this.circles = this.builder.container("circles"); + let boundaries = this.builder.in(this.circles).image("map-boundaries", 0, 0, true); + boundaries.setScale(starsystem.radius / (this.scaleX * 256)); - location_sprite = this.builder.button(`map-location-${loctype}`, location.x / this.scaleX, location.y / this.scaleY, - () => this.view.moveToLocation(location), - () => { - let visited = this.player.hasVisitedLocation(location); - let shop = (visited && !location.encounter && location.shop) ? " (dockyard present)" : ""; + // Show locations + starsystem.locations.map(location => { + let location_sprite: UIImage | UIButton | null = null; + let loctype = StarLocationType[location.type].toLowerCase(); - if (location.is(this.player.fleet.location)) { - return `Current fleet location${shop}`; - } else { - let danger = (visited && location.encounter) ? " [enemy fleet detected !]" : ""; - return `${visited ? "Visited" : "Unvisited"} ${loctype} - Move the fleet there${danger}${shop}`; - } - }, undefined, { center: true }); + location_sprite = this.builder.button(`map-location-${loctype}`, location.x / this.scaleX, location.y / this.scaleY, + () => this.view.moveToLocation(location), + () => { + let visited = this.player.hasVisitedLocation(location); + let shop = (visited && !location.encounter && location.shop) ? " (dockyard present)" : ""; - location_sprite.setRotation(Math.atan2(location.y, location.x)); - if (location.type == StarLocationType.PLANET) { - this.addOrbit(location.x, location.y); - } + if (location.is(this.player.fleet.location)) { + return `Current fleet location${shop}`; + } else { + let danger = (visited && location.encounter) ? " [enemy fleet detected !]" : ""; + return `${visited ? "Visited" : "Unvisited"} ${loctype} - Move the fleet there${danger}${shop}`; + } + }, undefined, { center: true }); - let status = this.getBadgeFrame(location); - let status_badge = this.builder.image(`map-status-${status}`, (location.x + 0.005) / this.scaleX, (location.y + 0.005) / this.scaleY, true); - this.locations.push([location, location_sprite, status_badge]); - }); + location_sprite.setRotation(Math.atan2(location.y, location.x)); + if (location.type == StarLocationType.PLANET) { + this.addOrbit(location.x, location.y); + } - // Show name - this.label = this.builder.button("map-name", 0, 460, undefined, `Level ${this.starsystem.level} starsystem`, undefined, { center: true }); - this.builder.in(this.label, builder => { - builder.text(this.starsystem.name, -30, 0, { size: 32, color: "#b8d2f1" }); - builder.text(this.starsystem.level.toString(), 243, 30, { size: 24, color: "#a0a0a0" }); - }); - } + let status = this.getBadgeFrame(location); + let status_badge = this.builder.image(`map-status-${status}`, (location.x + 0.005) / this.scaleX, (location.y + 0.005) / this.scaleY, true); + this.locations.push([location, location_sprite, status_badge]); + }); - /** - * Add an orbit marker - */ - addOrbit(x: number, y: number): void { - let radius = Math.sqrt(x * x + y * y); - let angle = Math.atan2(y, x); + // Show name + this.label = this.builder.button("map-name", 0, 460, undefined, `Level ${this.starsystem.level} starsystem`, undefined, { center: true }); + this.builder.in(this.label, builder => { + builder.text(this.starsystem.name, -30, 0, { size: 32, color: "#b8d2f1" }); + builder.text(this.starsystem.level.toString(), 243, 30, { size: 24, color: "#a0a0a0" }); + }); + } - let circle = this.builder.in(this.circles).image("map-orbit", 0, 0, true); - circle.setScale(radius / (this.scaleX * 198)); - circle.rotation = angle - 0.01; - } + /** + * Add an orbit marker + */ + addOrbit(x: number, y: number): void { + let radius = Math.sqrt(x * x + y * y); + let angle = Math.atan2(y, x); - /** - * Return the frame to use for info badge. - */ - getBadgeFrame(location: StarLocation): string { - if (this.player.hasVisitedLocation(location)) { - if (location.encounter) { - return "enemy"; - } else if (location.shop) { - return "dockyard"; - } else { - return "clear"; - } - } else { - return "unvisited"; - } - } + let circle = this.builder.in(this.circles).image("map-orbit", 0, 0, true); + circle.setScale(radius / (this.scaleX * 198)); + circle.rotation = angle - 0.01; + } - /** - * Update displayed information, and fog of war - */ - updateInfo(level: number, focus: boolean) { - this.locations.forEach(info => { - let status = this.getBadgeFrame(info[0]); - this.view.changeImage(info[2], `map-status-${status}`); - }); - - // LOD - let detailed = focus && level == 2; - this.list.filter(child => child !== this.label).forEach(child => { - if (child !== this.label && child !== this.background && (child instanceof UIButton || child instanceof UIImage)) { - this.view.animations.setVisible(child, detailed, 300); - } - }); - - this.updateLabel(level); - } - - /** - * Update label position and scaling - */ - updateLabel(zoom: number) { - this.label.visible = this.player.hasVisitedSystem(this.starsystem); - - let factor = (zoom == 2) ? 1 : (zoom == 1 ? 5 : 15); - let position = (zoom == 2) ? { x: -680, y: 440 } : { x: 0, y: (zoom == 1 ? 180 : 100) * factor }; - this.view.animations.addAnimation(this.label, { x: position.x, y: position.y, scaleX: factor, scaleY: factor }, 500, "Cubic.easeInOut"); - } + /** + * Return the frame to use for info badge. + */ + getBadgeFrame(location: StarLocation): string { + if (this.player.hasVisitedLocation(location)) { + if (location.encounter) { + return "enemy"; + } else if (location.shop) { + return "dockyard"; + } else { + return "clear"; + } + } else { + return "unvisited"; } + } + + /** + * Update displayed information, and fog of war + */ + updateInfo(level: number, focus: boolean) { + this.locations.forEach(info => { + let status = this.getBadgeFrame(info[0]); + this.view.changeImage(info[2], `map-status-${status}`); + }); + + // LOD + let detailed = focus && level == 2; + this.list.filter(child => child !== this.label).forEach(child => { + if (child !== this.label && child !== this.background && (child instanceof UIButton || child instanceof UIImage)) { + this.view.animations.setVisible(child, detailed, 300); + } + }); + + this.updateLabel(level); + } + + /** + * Update label position and scaling + */ + updateLabel(zoom: number) { + this.label.visible = this.player.hasVisitedSystem(this.starsystem); + + let factor = (zoom == 2) ? 1 : (zoom == 1 ? 5 : 15); + let position = (zoom == 2) ? { x: -680, y: 440 } : { x: 0, y: (zoom == 1 ? 180 : 100) * factor }; + this.view.animations.addAnimation(this.label, { x: position.x, y: position.y, scaleX: factor, scaleY: factor }, 500, "Cubic.easeInOut"); + } } diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index f7a286b..6fc8298 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -1,355 +1,372 @@ -/// +import { max, min } from "../../common/Tools" +import { Player } from "../../core/Player" +import { Star } from "../../core/Star" +import { StarLink } from "../../core/StarLink" +import { StarLocation, StarLocationType } from "../../core/StarLocation" +import { Universe } from "../../core/Universe" +import { BaseView } from "../BaseView" +import { CharacterSheet, CharacterSheetMode } from "../character/CharacterSheet" +import { UIBuilder } from "../common/UIBuilder" +import { UIButton } from "../common/UIButton" +import { UIContainer } from "../common/UIContainer" +import { UIGraphics } from "../common/UIGraphics" +import { ActiveMissionsDisplay } from "./ActiveMissionsDisplay" +import { CurrentLocationMarker } from "./CurrentLocationMarker" +import { FleetDisplay } from "./FleetDisplay" +import { MapLocationMenu } from "./MapLocationMenu" +import { MissionConversationDisplay } from "./MissionConversationDisplay" +import { MissionLocationMarker } from "./MissionLocationMarker" +import { MissionsDialog } from "./MissionsDialog" +import { StarSystemDisplay } from "./StarSystemDisplay" -module TK.SpaceTac.UI { - /** - * Interactive map of the universe - */ - export class UniverseMapView extends BaseView { - // Displayed universe - universe = new Universe() +/** + * Interactive map of the universe + */ +export class UniverseMapView extends BaseView { + // Displayed universe + universe = new Universe() - // Interacting player - player = new Player() + // Interacting player + player = new Player() - // Interaction enabled or not - interactive = true + // Interaction enabled or not + interactive = true - // Layers - layer_universe!: UIContainer - layer_overlay!: UIContainer + // Layers + layer_universe!: UIContainer + layer_overlay!: UIContainer - // Star systems - starsystems: StarSystemDisplay[] = [] + // Star systems + starsystems: StarSystemDisplay[] = [] - // Links between stars - starlinks_group!: UIContainer - starlinks: UIGraphics[] = [] + // Links between stars + starlinks_group!: UIContainer + starlinks: UIGraphics[] = [] - // Fleets - player_fleet!: FleetDisplay + // Fleets + player_fleet!: FleetDisplay - // Markers - current_location!: CurrentLocationMarker - mission_markers!: MissionLocationMarker + // Markers + current_location!: CurrentLocationMarker + mission_markers!: MissionLocationMarker - // Actions for selected location - actions!: MapLocationMenu + // Actions for selected location + actions!: MapLocationMenu - // Active missions - missions!: ActiveMissionsDisplay - conversation!: MissionConversationDisplay + // Active missions + missions!: ActiveMissionsDisplay + conversation!: MissionConversationDisplay - // Character sheet - character_sheet!: CharacterSheet + // Character sheet + character_sheet!: CharacterSheet - // Zoom level - zoom = 0 - zoom_in!: UIButton - zoom_out!: UIButton + // Zoom level + zoom = 0 + zoom_in!: UIButton + zoom_out!: UIButton - // Options button - button_options!: UIButton + // Options button + button_options!: UIButton - /** - * Init the view, binding it to a universe - */ - init(data: { universe: Universe, player: Player }) { - super.init(data); + /** + * Init the view, binding it to a universe + */ + init(data: { universe: Universe, player: Player }) { + super.init(data); - this.universe = data.universe; - this.player = data.player; - } + this.universe = data.universe; + this.player = data.player; + } - /** - * Create view graphics - */ - create() { - super.create(); + /** + * Create view graphics + */ + create() { + super.create(); - let builder = new UIBuilder(this); + let builder = new UIBuilder(this); - this.layer_universe = this.getLayer("universe"); - this.layer_overlay = this.getLayer("overlay"); + this.layer_universe = this.getLayer("universe"); + this.layer_overlay = this.getLayer("overlay"); - this.starlinks_group = builder.in(this.layer_universe).container("starlinks"); - this.starlinks = []; - this.starlinks = this.universe.starlinks.map(starlink => { - let loc1 = starlink.first.getWarpLocationTo(starlink.second); - let loc2 = starlink.second.getWarpLocationTo(starlink.first); + this.starlinks_group = builder.in(this.layer_universe).container("starlinks"); + this.starlinks = []; + this.starlinks = this.universe.starlinks.map(starlink => { + let loc1 = starlink.first.getWarpLocationTo(starlink.second); + let loc2 = starlink.second.getWarpLocationTo(starlink.first); - let result = builder.in(this.starlinks_group).graphics("starlink"); - if (loc1 && loc2) { - result.addLine({ - start: { x: starlink.first.x + loc1.x, y: starlink.first.y + loc1.y }, - end: { x: starlink.second.x + loc2.x, y: starlink.second.y + loc2.y }, - color: 0x6cc7ce, - width: 0.01, - }); - } - result.setDataEnabled(); - result.data.set("link", starlink); - return result; - }); + let result = builder.in(this.starlinks_group).graphics("starlink"); + if (loc1 && loc2) { + result.addLine({ + start: { x: starlink.first.x + loc1.x, y: starlink.first.y + loc1.y }, + end: { x: starlink.second.x + loc2.x, y: starlink.second.y + loc2.y }, + color: 0x6cc7ce, + width: 0.01, + }); + } + result.setDataEnabled(); + result.data.set("link", starlink); + return result; + }); - this.starsystems = this.universe.stars.map(star => new StarSystemDisplay(this, star)); - this.starsystems.forEach(starsystem => this.layer_universe.add(starsystem)); + this.starsystems = this.universe.stars.map(star => new StarSystemDisplay(this, star)); + this.starsystems.forEach(starsystem => this.layer_universe.add(starsystem)); - this.player_fleet = new FleetDisplay(this, this.player.fleet, this.universe, this.current_location); - this.layer_universe.add(this.player_fleet); + this.player_fleet = new FleetDisplay(this, this.player.fleet, this.universe, this.current_location); + this.layer_universe.add(this.player_fleet); - this.current_location = new CurrentLocationMarker(this, this.player_fleet); - this.layer_universe.add(this.current_location); + this.current_location = new CurrentLocationMarker(this, this.player_fleet); + this.layer_universe.add(this.current_location); - this.mission_markers = new MissionLocationMarker(this, this.layer_universe); + this.mission_markers = new MissionLocationMarker(this, this.layer_universe); - this.actions = new MapLocationMenu(this, this.layer_overlay, 30, 30); + this.actions = new MapLocationMenu(this, this.layer_overlay, 30, 30); - this.missions = new ActiveMissionsDisplay(this, this.player.missions, this.mission_markers); - this.missions.moveTo(this.layer_overlay, 20, 720); + this.missions = new ActiveMissionsDisplay(this, this.player.missions, this.mission_markers); + this.missions.moveTo(this.layer_overlay, 20, 720); - builder.in(this.layer_overlay, builder => { - this.zoom_in = builder.button("map-zoom-in", 1787, 54, () => this.setZoom(this.zoom + 1), "Zoom in"); - this.zoom_out = builder.button("map-zoom-out", 1787, 840, () => this.setZoom(this.zoom - 1), "Zoom out"); - this.button_options = builder.button("map-options", 1628, 0, () => this.showOptions(), "Game options"); - }); + builder.in(this.layer_overlay, builder => { + this.zoom_in = builder.button("map-zoom-in", 1787, 54, () => this.setZoom(this.zoom + 1), "Zoom in"); + this.zoom_out = builder.button("map-zoom-out", 1787, 840, () => this.setZoom(this.zoom - 1), "Zoom out"); + this.button_options = builder.button("map-options", 1628, 0, () => this.showOptions(), "Game options"); + }); - this.character_sheet = new CharacterSheet(this, CharacterSheetMode.EDITION); - this.character_sheet.moveToLayer(this.layer_overlay); + this.character_sheet = new CharacterSheet(this, CharacterSheetMode.EDITION); + this.character_sheet.moveToLayer(this.layer_overlay); - this.conversation = new MissionConversationDisplay(builder.in(this.layer_overlay)); + this.conversation = new MissionConversationDisplay(builder.in(this.layer_overlay)); - this.audio.startMusic("spring-thaw"); + this.audio.startMusic("spring-thaw"); - // Inputs - this.inputs.bind(" ", "Conversation step", () => this.conversation.forward()); - this.inputs.bind("Escape", "Skip conversation", () => this.conversation.skipConversation()); - this.inputs.bindCheat("r", "Reveal whole map", () => this.revealAll()); - this.inputs.bindCheat("u", "Level up", () => { - this.player.fleet.ships.forEach(ship => ship.level.forceLevelUp()); - this.refresh(); - }); - this.inputs.bindCheat("n", "Next story step", () => { - if (this.player.missions.main) { - this.player.missions.main.current_part.forceComplete(); - this.backToRouter(); - } - }); + // Inputs + this.inputs.bind(" ", "Conversation step", () => this.conversation.forward()); + this.inputs.bind("Escape", "Skip conversation", () => this.conversation.skipConversation()); + this.inputs.bindCheat("r", "Reveal whole map", () => this.revealAll()); + this.inputs.bindCheat("u", "Level up", () => { + this.player.fleet.ships.forEach(ship => ship.level.forceLevelUp()); + this.refresh(); + }); + this.inputs.bindCheat("n", "Next story step", () => { + if (this.player.missions.main) { + this.player.missions.main.current_part.forceComplete(); + this.backToRouter(); + } + }); - this.setZoom(2, 0); + this.setZoom(2, 0); - // Add a background - //builder.image("map-background"); + // Add a background + //builder.image("map-background"); - // Trigger an auto-save any time we go back to the map - this.autoSave(); - } + // Trigger an auto-save any time we go back to the map + this.autoSave(); + } - /** - * Leaving the view, unbind and destroy - */ - shutdown() { - this.universe = new Universe(); - this.player = new Player(); + /** + * Leaving the view, unbind and destroy + */ + shutdown() { + this.universe = new Universe(); + this.player = new Player(); - super.shutdown(); - } + super.shutdown(); + } - /** - * Refresh the view - */ - refresh() { - if (this.player.getBattle()) { - this.backToRouter(); - } else { - this.setZoom(this.zoom); - this.character_sheet.refresh(); - this.player_fleet.updateShipSprites(); - } - } - - /** - * Check active missions. - * - * When any mission status changes, a refresh is triggered. - */ - checkMissionsUpdate() { - if (this.missions.checkUpdate()) { - this.refresh(); - } - } - - /** - * Update info on all star systems (fog of war, available data...) - */ - updateInfo(current_star: Star | null, interactive = true) { - this.current_location.setZoom(this.zoom); - if (current_star) { - this.mission_markers.setZoom(this.zoom, current_star); - } - - this.starlinks.forEach(linkgraphics => { - let link = linkgraphics.data.get("link"); - if (link instanceof StarLink) { - linkgraphics.setVisible(this.player.hasVisitedSystem(link.first) || this.player.hasVisitedSystem(link.second)); - } - }) - - this.starsystems.forEach(system => system.updateInfo(this.zoom, system.starsystem == current_star)); - - this.actions.setFromLocation(this.session.getLocation(), this); - - this.missions.checkUpdate(); - this.conversation.updateFromMissions(this.player.missions, () => this.checkMissionsUpdate()); - - if (interactive) { - this.setInteractionEnabled(true); - } - } - - /** - * Reveal the whole map (this is a cheat) - */ - revealAll(): void { - this.universe.stars.forEach(star => { - star.locations.forEach(location => { - this.player.fleet.setVisited(location); - }); - }); - this.refresh(); - } - - /** - * Set the camera to center on a target, and to display a given span in height - */ - setCamera(x: number, y: number, span: number, duration = 500, easing = "Cubic.easeInOut") { - let scale = 1000 / span; - let dest_x = 920 - x * scale; - let dest_y = 540 - y * scale; - if (duration) { - this.animations.addAnimation(this.layer_universe, { x: dest_x, y: dest_y, scaleX: scale, scaleY: scale }, duration, easing); - } else { - this.layer_universe.setPosition(dest_x, dest_y); - this.layer_universe.setScale(scale); - } - } - - /** - * Set the camera to include all direct-jump accessible stars - */ - setCameraOnAccessible(star: Star, duration: number) { - let accessible = star.getNeighbors().concat([star]); - let xmin = min(accessible.map(star => star.x)); - let xmax = max(accessible.map(star => star.x)); - let ymin = min(accessible.map(star => star.y)); - let ymax = max(accessible.map(star => star.y)); - let dmax = Math.max(xmax - xmin, ymax - ymin); - this.setCamera(xmin + (xmax - xmin) * 0.5, ymin + (ymax - ymin) * 0.5, dmax * 1.2, duration); - } - - /** - * Set the alpha value for all links - */ - setLinksAlpha(alpha: number, duration = 500) { - if (duration) { - this.animations.addAnimation(this.starlinks_group, { alpha: alpha }, duration, "Cubic.easeInOut"); - } else { - this.starlinks_group.setAlpha(alpha); - } - } - - /** - * Set the current zoom level (0, 1 or 2) - */ - setZoom(level: number, duration = 500) { - let current_star = this.session.getLocation().star; - if (!current_star || level <= 0) { - this.setCamera(0, 0, this.universe.radius * 2, duration); - this.setLinksAlpha(1, duration); - this.zoom = 0; - } else if (level == 1) { - this.setCameraOnAccessible(current_star, duration); - this.setLinksAlpha(0.6, duration); - this.zoom = 1; - } else { - this.setCamera(current_star.x - current_star.radius * 0.3, current_star.y, current_star.radius * 2, duration); - this.setLinksAlpha(0.2, duration); - this.zoom = 2; - } - - this.updateInfo(current_star); - } - - /** - * Do the jump animation to another system - * - * This will only work if current location is a warp - */ - doJump(): void { - let location = this.session.getLocation(); - if (this.interactive && location && location.type == StarLocationType.WARP && location.jump_dest) { - let dest_location = location.jump_dest; - let dest_star = dest_location.star; - this.player_fleet.moveToLocation(dest_location, 3, duration => { - this.player_fleet.showJumpEffect(location.getDistanceTo(dest_location), duration); - this.timer.schedule(duration * 0.3, () => this.updateInfo(dest_star, false)); - this.timer.schedule(duration * 0.7, () => this.player_fleet.showJumpEffect()); - this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, "Cubic.easeOut"); - }, () => { - this.setInteractionEnabled(true); - this.refresh(); - }); - this.setInteractionEnabled(false); - } - } - - /** - * Open the dockyard interface - * - * This will only work if current location has a dockyard - */ - openShop(): void { - let location = this.session.getLocation(); - if (this.interactive && location && location.shop) { - this.character_sheet.show(this.player.fleet.ships[0]); - } - } - - /** - * Open the missions dialog (job posting) - * - * This will only work if current location has a dockyard - */ - openMissions(): void { - let location = this.session.getLocation(); - if (this.interactive && location && location.shop) { - new MissionsDialog(this, location.shop, this.player, () => this.checkMissionsUpdate()); - } - } - - /** - * Move the fleet to another location - */ - moveToLocation(dest: StarLocation): void { - if (this.interactive && !dest.is(this.player.fleet.location)) { - this.setInteractionEnabled(false); - this.player_fleet.moveToLocation(dest, 1, null, () => { - this.setInteractionEnabled(true); - this.refresh(); - }); - } - } - - /** - * Set the interactive state - */ - setInteractionEnabled(enabled: boolean) { - this.interactive = enabled && !this.session.spectator; - this.animations.setVisible(this.actions.container, enabled && this.zoom == 2, 300); - this.missions.setVisible(enabled && this.zoom == 2, 300); - this.animations.setVisible(this.zoom_in, enabled && this.zoom < 2, 300); - this.animations.setVisible(this.zoom_out, enabled && this.zoom > 0, 300); - this.animations.setVisible(this.button_options, enabled, 300); - //this.animations.setVisible(this.character_sheet, enabled, 300); - } + /** + * Refresh the view + */ + refresh() { + if (this.player.getBattle()) { + this.backToRouter(); + } else { + this.setZoom(this.zoom); + this.character_sheet.refresh(); + this.player_fleet.updateShipSprites(); } + } + + /** + * Check active missions. + * + * When any mission status changes, a refresh is triggered. + */ + checkMissionsUpdate() { + if (this.missions.checkUpdate()) { + this.refresh(); + } + } + + /** + * Update info on all star systems (fog of war, available data...) + */ + updateInfo(current_star: Star | null, interactive = true) { + this.current_location.setZoom(this.zoom); + if (current_star) { + this.mission_markers.setZoom(this.zoom, current_star); + } + + this.starlinks.forEach(linkgraphics => { + let link = linkgraphics.data.get("link"); + if (link instanceof StarLink) { + linkgraphics.setVisible(this.player.hasVisitedSystem(link.first) || this.player.hasVisitedSystem(link.second)); + } + }) + + this.starsystems.forEach(system => system.updateInfo(this.zoom, system.starsystem == current_star)); + + this.actions.setFromLocation(this.session.getLocation(), this); + + this.missions.checkUpdate(); + this.conversation.updateFromMissions(this.player.missions, () => this.checkMissionsUpdate()); + + if (interactive) { + this.setInteractionEnabled(true); + } + } + + /** + * Reveal the whole map (this is a cheat) + */ + revealAll(): void { + this.universe.stars.forEach(star => { + star.locations.forEach(location => { + this.player.fleet.setVisited(location); + }); + }); + this.refresh(); + } + + /** + * Set the camera to center on a target, and to display a given span in height + */ + setCamera(x: number, y: number, span: number, duration = 500, easing = "Cubic.easeInOut") { + let scale = 1000 / span; + let dest_x = 920 - x * scale; + let dest_y = 540 - y * scale; + if (duration) { + this.animations.addAnimation(this.layer_universe, { x: dest_x, y: dest_y, scaleX: scale, scaleY: scale }, duration, easing); + } else { + this.layer_universe.setPosition(dest_x, dest_y); + this.layer_universe.setScale(scale); + } + } + + /** + * Set the camera to include all direct-jump accessible stars + */ + setCameraOnAccessible(star: Star, duration: number) { + let accessible = star.getNeighbors().concat([star]); + let xmin = min(accessible.map(star => star.x)); + let xmax = max(accessible.map(star => star.x)); + let ymin = min(accessible.map(star => star.y)); + let ymax = max(accessible.map(star => star.y)); + let dmax = Math.max(xmax - xmin, ymax - ymin); + this.setCamera(xmin + (xmax - xmin) * 0.5, ymin + (ymax - ymin) * 0.5, dmax * 1.2, duration); + } + + /** + * Set the alpha value for all links + */ + setLinksAlpha(alpha: number, duration = 500) { + if (duration) { + this.animations.addAnimation(this.starlinks_group, { alpha: alpha }, duration, "Cubic.easeInOut"); + } else { + this.starlinks_group.setAlpha(alpha); + } + } + + /** + * Set the current zoom level (0, 1 or 2) + */ + setZoom(level: number, duration = 500) { + let current_star = this.session.getLocation().star; + if (!current_star || level <= 0) { + this.setCamera(0, 0, this.universe.radius * 2, duration); + this.setLinksAlpha(1, duration); + this.zoom = 0; + } else if (level == 1) { + this.setCameraOnAccessible(current_star, duration); + this.setLinksAlpha(0.6, duration); + this.zoom = 1; + } else { + this.setCamera(current_star.x - current_star.radius * 0.3, current_star.y, current_star.radius * 2, duration); + this.setLinksAlpha(0.2, duration); + this.zoom = 2; + } + + this.updateInfo(current_star); + } + + /** + * Do the jump animation to another system + * + * This will only work if current location is a warp + */ + doJump(): void { + let location = this.session.getLocation(); + if (this.interactive && location && location.type == StarLocationType.WARP && location.jump_dest) { + let dest_location = location.jump_dest; + let dest_star = dest_location.star; + this.player_fleet.moveToLocation(dest_location, 3, duration => { + this.player_fleet.showJumpEffect(location.getDistanceTo(dest_location), duration); + this.timer.schedule(duration * 0.3, () => this.updateInfo(dest_star, false)); + this.timer.schedule(duration * 0.7, () => this.player_fleet.showJumpEffect()); + this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, "Cubic.easeOut"); + }, () => { + this.setInteractionEnabled(true); + this.refresh(); + }); + this.setInteractionEnabled(false); + } + } + + /** + * Open the dockyard interface + * + * This will only work if current location has a dockyard + */ + openShop(): void { + let location = this.session.getLocation(); + if (this.interactive && location && location.shop) { + this.character_sheet.show(this.player.fleet.ships[0]); + } + } + + /** + * Open the missions dialog (job posting) + * + * This will only work if current location has a dockyard + */ + openMissions(): void { + let location = this.session.getLocation(); + if (this.interactive && location && location.shop) { + new MissionsDialog(this, location.shop, this.player, () => this.checkMissionsUpdate()); + } + } + + /** + * Move the fleet to another location + */ + moveToLocation(dest: StarLocation): void { + if (this.interactive && !dest.is(this.player.fleet.location)) { + this.setInteractionEnabled(false); + this.player_fleet.moveToLocation(dest, 1, null, () => { + this.setInteractionEnabled(true); + this.refresh(); + }); + } + } + + /** + * Set the interactive state + */ + setInteractionEnabled(enabled: boolean) { + this.interactive = enabled && !this.session.spectator; + this.animations.setVisible(this.actions.container, enabled && this.zoom == 2, 300); + this.missions.setVisible(enabled && this.zoom == 2, 300); + this.animations.setVisible(this.zoom_in, enabled && this.zoom < 2, 300); + this.animations.setVisible(this.zoom_out, enabled && this.zoom > 0, 300); + this.animations.setVisible(this.button_options, enabled, 300); + //this.animations.setVisible(this.character_sheet, enabled, 300); + } } diff --git a/src/ui/menu/InputInviteCode.spec.ts b/src/ui/menu/InputInviteCode.spec.ts index 45f4f67..3024760 100644 --- a/src/ui/menu/InputInviteCode.spec.ts +++ b/src/ui/menu/InputInviteCode.spec.ts @@ -1,27 +1,31 @@ -module TK.SpaceTac.UI.Specs { - testing("InputInviteCode", test => { - let testgame = setupEmptyView(test); +import { testing } from "../../common/Testing"; +import { GameSession } from "../../core/GameSession"; +import { UIBuilder } from "../common/UIBuilder"; +import { setupEmptyView } from "../TestGame"; +import { InputInviteCode } from "./InputInviteCode"; - test.acase("joins remote sessions as spectator", async check => { - return new Promise((resolve, reject) => { - let view = testgame.view; - let session = new GameSession(); - check.equals(session.primary, true); - check.equals(session.spectator, false); - view.getConnection().publish(session, "Test").then(token => { - let dialog = new InputInviteCode(view, new UIBuilder(view), 0, 0); - dialog.token_input.setContent(token); +testing("InputInviteCode", test => { + let testgame = setupEmptyView(test); - check.patch(view.gameui, "setSession", (joined: GameSession) => { - test.check.equals(joined.id, session.id); - check.equals(joined.primary, false); - check.equals(joined.spectator, true); - resolve(); - }); + test.acase("joins remote sessions as spectator", async check => { + return new Promise((resolve, reject) => { + let view = testgame.view; + let session = new GameSession(); + check.equals(session.primary, true); + check.equals(session.spectator, false); + view.getConnection().publish(session, "Test").then(token => { + let dialog = new InputInviteCode(view, new UIBuilder(view), 0, 0); + dialog.token_input.setContent(token); - dialog.join(); - }); - }); + check.patch(view.gameui, "setSession", (joined: GameSession) => { + test.check.equals(joined.id, session.id); + check.equals(joined.primary, false); + check.equals(joined.spectator, true); + resolve(); }); + + dialog.join(); + }); }); -} + }); +}); diff --git a/src/ui/menu/InputInviteCode.ts b/src/ui/menu/InputInviteCode.ts index 6f7c01f..e636ad5 100644 --- a/src/ui/menu/InputInviteCode.ts +++ b/src/ui/menu/InputInviteCode.ts @@ -1,30 +1,32 @@ -module TK.SpaceTac.UI { - /** - * Input to display available save games, and load one - */ - export class InputInviteCode { - token_input: UITextInput +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UITextInput } from "../common/UITextInput"; - constructor(private view: BaseView, private builder: UIBuilder, x: number, y: number) { - this.token_input = new UITextInput(builder, "menu-input", x, y, 8, "Invite code"); - } +/** + * Input to display available save games, and load one + */ +export class InputInviteCode { + token_input: UITextInput - /** - * Join an online game - */ - join(): void { - let token = this.token_input.getContent(); - let connection = this.view.getConnection(); + constructor(private view: BaseView, private builder: UIBuilder, x: number, y: number) { + this.token_input = new UITextInput(builder, "menu-input", x, y, 8, "Invite code"); + } - connection.loadByToken(token).then(session => { - if (session) { - // For now, we will only spectate - session.primary = false; - session.spectator = true; + /** + * Join an online game + */ + join(): void { + let token = this.token_input.getContent(); + let connection = this.view.getConnection(); - this.view.gameui.setSession(session, token); - } - }); - } - } + connection.loadByToken(token).then(session => { + if (session) { + // For now, we will only spectate + session.primary = false; + session.spectator = true; + + this.view.gameui.setSession(session, token); + } + }); + } } diff --git a/src/ui/menu/InputSavegames.ts b/src/ui/menu/InputSavegames.ts index dec21b6..f29eac3 100644 --- a/src/ui/menu/InputSavegames.ts +++ b/src/ui/menu/InputSavegames.ts @@ -1,83 +1,87 @@ -module TK.SpaceTac.UI { - /** - * Input to display available save games, and load one - */ - export class InputSavegames { - private saves: [string, string][] = [] - private save_selected = 0 - private save_name?: UIText +import { clamp, cmp, items } from "../../common/Tools"; +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { UIText } from "../common/UIText"; +import { UIWaitingDialog } from "../common/UIWaitingDialog"; - constructor(private view: BaseView, private builder: UIBuilder, x: number, y: number) { - builder.in(builder.image("menu-input", x, y, true), builder => { - builder.button("menu-arrow-left", -196, 0, () => this.paginateSave(1), "Older saves", undefined, { center: true }); - builder.button("menu-arrow-right", 196, 0, () => this.paginateSave(-1), "Newer saves", undefined, { center: true }); +/** + * Input to display available save games, and load one + */ +export class InputSavegames { + private saves: [string, string][] = [] + private save_selected = 0 + private save_name?: UIText - this.save_name = builder.text("", 0, 0, { size: 24 }); - }); + constructor(private view: BaseView, private builder: UIBuilder, x: number, y: number) { + builder.in(builder.image("menu-input", x, y, true), builder => { + builder.button("menu-arrow-left", -196, 0, () => this.paginateSave(1), "Older saves", undefined, { center: true }); + builder.button("menu-arrow-right", 196, 0, () => this.paginateSave(-1), "Newer saves", undefined, { center: true }); - this.refreshSaves(); - } + this.save_name = builder.text("", 0, 0, { size: 24 }); + }); - /** - * Refresh available save games - */ - refreshSaves(): void { - let connection = this.view.getConnection(); + this.refreshSaves(); + } - // TODO include local save - // TODO Disable interaction, with loading icon + /** + * Refresh available save games + */ + refreshSaves(): void { + let connection = this.view.getConnection(); - connection.listSaves().then(results => { - this.saves = items(results).sort(([id1, info1], [id2, info2]) => cmp(info2, info1)); - this.setCurrentSave(0); - }); - } + // TODO include local save + // TODO Disable interaction, with loading icon - /** - * Set the current selected save game - */ - setCurrentSave(position: number): void { - if (!this.save_name) { - return; - } + connection.listSaves().then(results => { + this.saves = items(results).sort(([id1, info1], [id2, info2]) => cmp(info2, info1)); + this.setCurrentSave(0); + }); + } - if (this.saves.length == 0) { - this.builder.change(this.save_name, "No save game found"); - } else { - this.save_selected = clamp(position, 0, this.saves.length - 1); - - let [saveid, saveinfo] = this.saves[this.save_selected]; - this.builder.change(this.save_name, saveinfo); - } - } - - /** - * Change the selected save - */ - paginateSave(offset: number) { - this.setCurrentSave(this.save_selected + offset); - } - - /** - * Load selected save game - */ - load(): void { - if (this.save_selected >= 0 && this.saves.length > this.save_selected) { - let connection = this.view.getConnection(); - let [saveid, saveinfo] = this.saves[this.save_selected]; - - let dialog = new UIWaitingDialog(this.view, "Loading game from server..."); - connection.loadById(saveid).then(session => { - if (session) { - this.view.gameui.setSession(session); - dialog.close(); - } else { - dialog.displayError("No suitable data found in save game (saved with older version ?)"); - } - }).catch(() => { - dialog.displayError("Error while loading game from server"); - }); - } - } + /** + * Set the current selected save game + */ + setCurrentSave(position: number): void { + if (!this.save_name) { + return; } + + if (this.saves.length == 0) { + this.builder.change(this.save_name, "No save game found"); + } else { + this.save_selected = clamp(position, 0, this.saves.length - 1); + + let [saveid, saveinfo] = this.saves[this.save_selected]; + this.builder.change(this.save_name, saveinfo); + } + } + + /** + * Change the selected save + */ + paginateSave(offset: number) { + this.setCurrentSave(this.save_selected + offset); + } + + /** + * Load selected save game + */ + load(): void { + if (this.save_selected >= 0 && this.saves.length > this.save_selected) { + let connection = this.view.getConnection(); + let [saveid, saveinfo] = this.saves[this.save_selected]; + + let dialog = new UIWaitingDialog(this.view, "Loading game from server..."); + connection.loadById(saveid).then(session => { + if (session) { + this.view.gameui.setSession(session); + dialog.close(); + } else { + dialog.displayError("No suitable data found in save game (saved with older version ?)"); + } + }).catch(() => { + dialog.displayError("Error while loading game from server"); + }); + } + } } diff --git a/src/ui/menu/MainMenu.ts b/src/ui/menu/MainMenu.ts index 13f2deb..70bf125 100644 --- a/src/ui/menu/MainMenu.ts +++ b/src/ui/menu/MainMenu.ts @@ -1,164 +1,165 @@ -/// +import { BaseView } from "../BaseView"; +import { UIBuilder } from "../common/UIBuilder"; +import { InputInviteCode } from "./InputInviteCode"; +import { InputSavegames } from "./InputSavegames"; -module TK.SpaceTac.UI { - /** - * Main menu (first interactive screen) - */ - export class MainMenu extends BaseView { - static returned = false +/** + * Main menu (first interactive screen) + */ +export class MainMenu extends BaseView { + static returned = false - create() { - super.create(); + create() { + super.create(); - let builder = new UIBuilder(this); + let builder = new UIBuilder(this); - // Layers - let layer_background = this.getLayer("background"); - let layer_presents = this.getLayer("presents"); - let layer_title = this.getLayer("title"); + // Layers + let layer_background = this.getLayer("background"); + let layer_presents = this.getLayer("presents"); + let layer_title = this.getLayer("title"); - // Background - builder.in(layer_background, builder => { - builder.image("menu-background"); - }); + // Background + builder.in(layer_background, builder => { + builder.image("menu-background"); + }); - // Presents... - builder.in(layer_presents, builder => { - builder.styled({ center: true, color: "#FFFFFF", shadow: true }, builder => { - builder.text("Michael Lemaire", this.getMidWidth(), this.getHeight() * 0.4, { size: 32 }); - builder.text("presents", this.getMidWidth(), this.getHeight() * 0.6, { size: 24 }); - }); - }); + // Presents... + builder.in(layer_presents, builder => { + builder.styled({ center: true, color: "#FFFFFF", shadow: true }, builder => { + builder.text("Michael Lemaire", this.getMidWidth(), this.getHeight() * 0.4, { size: 32 }); + builder.text("presents", this.getMidWidth(), this.getHeight() * 0.6, { size: 24 }); + }); + }); - builder.styled({ color: "#9fc4d6", size: 40, shadow: true }).in(layer_title, builder => { - // Title - let title = builder.in(layer_title).image("menu-title", 960, 784, true); + builder.styled({ color: "#9fc4d6", size: 40, shadow: true }).in(layer_title, builder => { + // Title + let title = builder.in(layer_title).image("menu-title", 960, 784, true); - // Buttons - let group_new_game = builder.container("new-game", 0, 0, false); - let group_load_game = builder.container("load-game", 0, 0, false); - let group_join_game = builder.container("join-game", 0, 0, false); - let group_skirmish = builder.in(group_new_game).container("skirmish", 0, 0, false); - let button_new_game = builder.button("menu-button", 280, 106, undefined, "Start a new game", (on: boolean) => { - if (on) { - this.animations.show(group_new_game, 200); - button_load_game.toggle(false); - button_join_game.toggle(false); - } else { - this.animations.hide(group_new_game, 200); - } - return on; - }, { text: "New game", center: true, on_bottom: true }); - let button_campaign = builder.in(group_new_game).button("menu-button", 770, 106, () => this.startCampaign(), "Start a campaign in story mode", undefined, { - text: "Campaign", center: true - }); - let button_skirmish = builder.in(group_new_game).button("menu-button", 770, 266, undefined, "Start a quick battle", (on: boolean) => { - this.animations.setVisible(group_skirmish, on, 200); - return on; - }, { text: "Skirmish", center: true, on_bottom: true }); - let button_skirmish_shipcount = this.addNumberSelector(builder.in(group_skirmish), 1130, 266, "Ships", 2, 5, 3); - let button_skirmish_level = this.addNumberSelector(builder.in(group_skirmish), 1386, 266, "Level", 1, 10, 1); - let button_skirmish_go = builder.in(group_skirmish).button("menu-button-small", 1632, 266, () => { - this.startSkirmish(button_skirmish_shipcount(), button_skirmish_level()) - }, "Start the skirmish with selected settings", undefined, { text: "Go", center: true }); - let button_load_game = builder.button("menu-button", 280, 266, undefined, "Load a previously saved game", (on: boolean) => { - if (on) { - this.animations.show(group_load_game, 200); - button_new_game.toggle(false); - button_join_game.toggle(false); - } else { - this.animations.hide(group_load_game, 200); - } - return on; - }, { text: "Load game", center: true, on_bottom: true }); - builder.in(group_load_game, builder => { - let input = new InputSavegames(this, builder, 770, 266); - builder.button("menu-button-small", 1112, 266, () => input.load(), "Load the selected save game", undefined, { - text: "Go", center: true - }); - }) - let button_join_game = builder.button("menu-button", 280, 426, undefined, "Join a friend's game", (on: boolean) => { - if (on) { - this.animations.show(group_join_game, 200); - button_new_game.toggle(false); - button_load_game.toggle(false); - } else { - this.animations.hide(group_join_game, 200); - } - return on; - }, { text: "Join game", center: true, on_bottom: true }); - builder.in(group_join_game, builder => { - let input = new InputInviteCode(this, builder, 770, 426); - builder.button("menu-button-small", 1112, 426, () => input.join(), "Join the game", undefined, { - text: "Go", center: true - }); - }) - let button_options = builder.button("menu-button-small", 1780, 106, () => this.showOptions(true), "Options", undefined, { - center: true, - icon: "menu-icon-options", - }); - }); - - // Animations - layer_background.visible = false; - layer_presents.visible = false; - layer_title.visible = false; - this.animations.show(layer_presents, 500); - this.animations.show(layer_background, 5000); - let fading = this.timer.schedule(5000, () => { - this.animations.show(layer_title, 1000); - this.animations.hide(layer_presents, 300); - }); - let pass = () => { - this.timer.cancel(fading); - this.animations.show(layer_background, 0); - this.animations.show(layer_title, 0); - this.animations.hide(layer_presents, 0); - }; - if (MainMenu.returned) { - pass(); - } else { - this.input.on("pointerup", pass); - MainMenu.returned = true; - } - - this.audio.startMusic("supernatural"); + // Buttons + let group_new_game = builder.container("new-game", 0, 0, false); + let group_load_game = builder.container("load-game", 0, 0, false); + let group_join_game = builder.container("join-game", 0, 0, false); + let group_skirmish = builder.in(group_new_game).container("skirmish", 0, 0, false); + let button_new_game = builder.button("menu-button", 280, 106, undefined, "Start a new game", (on: boolean) => { + if (on) { + this.animations.show(group_new_game, 200); + button_load_game.toggle(false); + button_join_game.toggle(false); + } else { + this.animations.hide(group_new_game, 200); } - - /** - * Add a number selector in a given range - */ - addNumberSelector(builder: UIBuilder, x: number, y: number, label: string, min: number, max: number, initial: number): () => number { - let value = initial; - builder.in(builder.image("menu-input-small", x, y + 30, true), builder => { - let display = builder.text(`${value}`, 0, -32); - builder.text(label, 0, 54, { color: "#6690a4", size: 28 }); - builder.button("menu-arrow-left", -68, -32, () => { - value = Math.max(min, value - 1); - builder.change(display, `${value}`); - }, undefined, undefined, { center: true }); - builder.button("menu-arrow-right", 68, -32, () => { - value = Math.min(max, value + 1); - builder.change(display, `${value}`); - }, undefined, undefined, { center: true }); - }); - return () => value; + return on; + }, { text: "New game", center: true, on_bottom: true }); + let button_campaign = builder.in(group_new_game).button("menu-button", 770, 106, () => this.startCampaign(), "Start a campaign in story mode", undefined, { + text: "Campaign", center: true + }); + let button_skirmish = builder.in(group_new_game).button("menu-button", 770, 266, undefined, "Start a quick battle", (on: boolean) => { + this.animations.setVisible(group_skirmish, on, 200); + return on; + }, { text: "Skirmish", center: true, on_bottom: true }); + let button_skirmish_shipcount = this.addNumberSelector(builder.in(group_skirmish), 1130, 266, "Ships", 2, 5, 3); + let button_skirmish_level = this.addNumberSelector(builder.in(group_skirmish), 1386, 266, "Level", 1, 10, 1); + let button_skirmish_go = builder.in(group_skirmish).button("menu-button-small", 1632, 266, () => { + this.startSkirmish(button_skirmish_shipcount(), button_skirmish_level()) + }, "Start the skirmish with selected settings", undefined, { text: "Go", center: true }); + let button_load_game = builder.button("menu-button", 280, 266, undefined, "Load a previously saved game", (on: boolean) => { + if (on) { + this.animations.show(group_load_game, 200); + button_new_game.toggle(false); + button_join_game.toggle(false); + } else { + this.animations.hide(group_load_game, 200); } - - /** - * Start a campaign mode - */ - startCampaign(): void { - this.session.startNewGame(false); - this.backToRouter(); + return on; + }, { text: "Load game", center: true, on_bottom: true }); + builder.in(group_load_game, builder => { + let input = new InputSavegames(this, builder, 770, 266); + builder.button("menu-button-small", 1112, 266, () => input.load(), "Load the selected save game", undefined, { + text: "Go", center: true + }); + }) + let button_join_game = builder.button("menu-button", 280, 426, undefined, "Join a friend's game", (on: boolean) => { + if (on) { + this.animations.show(group_join_game, 200); + button_new_game.toggle(false); + button_load_game.toggle(false); + } else { + this.animations.hide(group_join_game, 200); } + return on; + }, { text: "Join game", center: true, on_bottom: true }); + builder.in(group_join_game, builder => { + let input = new InputInviteCode(this, builder, 770, 426); + builder.button("menu-button-small", 1112, 426, () => input.join(), "Join the game", undefined, { + text: "Go", center: true + }); + }) + let button_options = builder.button("menu-button-small", 1780, 106, () => this.showOptions(true), "Options", undefined, { + center: true, + icon: "menu-icon-options", + }); + }); - /** - * Start a skirmish - */ - startSkirmish(shipcount: number, level: number): void { - this.session.startQuickBattle(true, level, shipcount); - this.backToRouter(); - } + // Animations + layer_background.visible = false; + layer_presents.visible = false; + layer_title.visible = false; + this.animations.show(layer_presents, 500); + this.animations.show(layer_background, 5000); + let fading = this.timer.schedule(5000, () => { + this.animations.show(layer_title, 1000); + this.animations.hide(layer_presents, 300); + }); + let pass = () => { + this.timer.cancel(fading); + this.animations.show(layer_background, 0); + this.animations.show(layer_title, 0); + this.animations.hide(layer_presents, 0); + }; + if (MainMenu.returned) { + pass(); + } else { + this.input.on("pointerup", pass); + MainMenu.returned = true; } + + this.audio.startMusic("supernatural"); + } + + /** + * Add a number selector in a given range + */ + addNumberSelector(builder: UIBuilder, x: number, y: number, label: string, min: number, max: number, initial: number): () => number { + let value = initial; + builder.in(builder.image("menu-input-small", x, y + 30, true), builder => { + let display = builder.text(`${value}`, 0, -32); + builder.text(label, 0, 54, { color: "#6690a4", size: 28 }); + builder.button("menu-arrow-left", -68, -32, () => { + value = Math.max(min, value - 1); + builder.change(display, `${value}`); + }, undefined, undefined, { center: true }); + builder.button("menu-arrow-right", 68, -32, () => { + value = Math.min(max, value + 1); + builder.change(display, `${value}`); + }, undefined, undefined, { center: true }); + }); + return () => value; + } + + /** + * Start a campaign mode + */ + startCampaign(): void { + this.session.startNewGame(false); + this.backToRouter(); + } + + /** + * Start a skirmish + */ + startSkirmish(shipcount: number, level: number): void { + this.session.startQuickBattle(true, level, shipcount); + this.backToRouter(); + } } diff --git a/src/ui/options/GameOptions.ts b/src/ui/options/GameOptions.ts index 12052d0..d9a9700 100644 --- a/src/ui/options/GameOptions.ts +++ b/src/ui/options/GameOptions.ts @@ -1,98 +1,98 @@ -module TK.SpaceTac.UI { - class GameOption { - code: string - current: T - getter: () => T - setter: (value: T) => any +import { MainUI } from "../../MainUI"; - constructor(code: string, getter: () => T, setter: (value: T) => any) { - this.code = code; - this.getter = getter; - this.setter = setter; - this.current = getter(); - } +class GameOption { + code: string + current: T + getter: () => T + setter: (value: T) => any - set(value: T) { - this.setter(value); - this.current = this.getter(); - } - } + constructor(code: string, getter: () => T, setter: (value: T) => any) { + this.code = code; + this.getter = getter; + this.setter = setter; + this.current = getter(); + } - /** - * Object to store and maintain game-wide options - * - * Options are kept on the browser storage when possible - */ - export class GameOptions { - booleans: { [code: string]: GameOption } - numbers: { [code: string]: GameOption } - - constructor(parent: MainUI) { - this.booleans = { - fullscreen: new GameOption("fullscreen", () => parent.isFullscreen(), value => parent.toggleFullscreen(value)), - } - this.numbers = { - mainvolume: new GameOption("mainvolume", () => parent.audio.getMainVolume(), value => parent.audio.setMainVolume(value)), - musicvolume: new GameOption("musicvolume", () => parent.audio.getMusicVolume(), value => parent.audio.setMusicVolume(value)), - } - } - - /** - * Get the current value of a boolean option - */ - getBooleanValue(code: string, default_value = false): boolean { - let option = this.booleans[code]; - if (option) { - return option.current; - } else { - return default_value; - } - } - - /** - * Set the current value of a boolean option - */ - setBooleanValue(code: string, value: boolean): boolean { - let option = this.booleans[code]; - if (option) { - option.set(value); - return true; - } else { - return false; - } - } - - /** - * Toggle a boolean value between true and false - */ - toggleBoolean(code: string): boolean { - this.setBooleanValue(code, !this.getBooleanValue(code)); - return this.getBooleanValue(code); - } - - /** - * Get the current value of a number option - */ - getNumberValue(code: string, default_value = 0): number { - let option = this.numbers[code]; - if (option) { - return option.current; - } else { - return default_value; - } - } - - /** - * Set the current value of a number option - */ - setNumberValue(code: string, value: number): boolean { - let option = this.numbers[code]; - if (option) { - option.set(value); - return true; - } else { - return false; - } - } - } + set(value: T) { + this.setter(value); + this.current = this.getter(); + } +} + +/** + * Object to store and maintain game-wide options + * + * Options are kept on the browser storage when possible + */ +export class GameOptions { + booleans: { [code: string]: GameOption } + numbers: { [code: string]: GameOption } + + constructor(parent: MainUI) { + this.booleans = { + fullscreen: new GameOption("fullscreen", () => parent.isFullscreen(), value => parent.toggleFullscreen(value)), + } + this.numbers = { + mainvolume: new GameOption("mainvolume", () => parent.audio.getMainVolume(), value => parent.audio.setMainVolume(value)), + musicvolume: new GameOption("musicvolume", () => parent.audio.getMusicVolume(), value => parent.audio.setMusicVolume(value)), + } + } + + /** + * Get the current value of a boolean option + */ + getBooleanValue(code: string, default_value = false): boolean { + let option = this.booleans[code]; + if (option) { + return option.current; + } else { + return default_value; + } + } + + /** + * Set the current value of a boolean option + */ + setBooleanValue(code: string, value: boolean): boolean { + let option = this.booleans[code]; + if (option) { + option.set(value); + return true; + } else { + return false; + } + } + + /** + * Toggle a boolean value between true and false + */ + toggleBoolean(code: string): boolean { + this.setBooleanValue(code, !this.getBooleanValue(code)); + return this.getBooleanValue(code); + } + + /** + * Get the current value of a number option + */ + getNumberValue(code: string, default_value = 0): number { + let option = this.numbers[code]; + if (option) { + return option.current; + } else { + return default_value; + } + } + + /** + * Set the current value of a number option + */ + setNumberValue(code: string, value: number): boolean { + let option = this.numbers[code]; + if (option) { + option.set(value); + return true; + } else { + return false; + } + } } diff --git a/src/ui/options/OptionsDialog.ts b/src/ui/options/OptionsDialog.ts index 825d747..6d61747 100644 --- a/src/ui/options/OptionsDialog.ts +++ b/src/ui/options/OptionsDialog.ts @@ -1,172 +1,173 @@ -/// +import { BaseView } from "../BaseView"; +import { BattleView } from "../battle/BattleView"; +import { UIButton } from "../common/UIButton"; +import { UIDialog } from "../common/UIDialog"; -module TK.SpaceTac.UI { - /** - * Dialog to display game options - */ - export class OptionsDialog extends UIDialog { - constructor(parent: BaseView, credits = false) { - super(parent); +/** + * Dialog to display game options + */ +export class OptionsDialog extends UIDialog { + constructor(parent: BaseView, credits = false) { + super(parent); - if (credits) { - this.pageCredits(); - } else { - this.pageMenu(); - } + if (credits) { + this.pageCredits(); + } else { + this.pageMenu(); + } - this.addCloseButton(); + this.addCloseButton(); + } + + pageCommon() { + let options = this.view.options; + + this.content.clear(); + + this.content.image("options-logo", 473, 71); + this.content.image("options-buttons-background", 244, 357); + + this.addToggleButton(415, 381, "options-option-sound", "Toggle all sound", + toggled => options.setNumberValue("mainvolume", toggled ? 1 : 0), + options.getNumberValue("mainvolume") > 0 + ); + + this.addToggleButton(this.width / 2, 381, "options-option-music", "Toggle music", + toggled => options.setNumberValue("musicvolume", toggled ? 1 : 0), + options.getNumberValue("musicvolume") > 0 + ); + + this.addToggleButton(this.width - 415, 381, "options-option-fullscreen", "Toggle fullscreen", + toggled => options.setBooleanValue("fullscreen", toggled), + options.getBooleanValue("fullscreen") + ); + } + + /** + * Add a toggle button + */ + addToggleButton(x: number, y: number, key: string, tooltip: string, callback: (state: boolean) => boolean, initial: boolean): UIButton { + let button = this.content.button("options-toggle", x, y, undefined, tooltip, state => { + if (callback(state)) { + return state; + } else { + return !state; + } + }, { center: true }); + this.content.in(button).image(key, 0, 0, true); + button.toggle(initial); + return button; + } + + /** + * Show the options menu + */ + pageMenu() { + this.pageCommon(); + + if (this.view.session.primary) { + this.content.button("options-button", this.width / 2, 600, () => this.pageInvite(), "Invite a friend to join this game as spectator", undefined, { + center: true, + text: "Invite a friend", + text_style: { + color: "#9fc4d6", + size: 36, } + }); + } - pageCommon() { - let options = this.view.options; + this.content.button("options-button", this.width / 2, 800, () => this.view.gameui.quitGame(), "End the current game and go back to main menu", undefined, { + center: true, + text: "Quit to menu", + text_style: { + color: "#9fc4d6", + size: 36, + } + }); + } - this.content.clear(); + /** + * Show the credits info + */ + pageCredits() { + this.pageCommon(); - this.content.image("options-logo", 473, 71); - this.content.image("options-buttons-background", 244, 357); - - this.addToggleButton(415, 381, "options-option-sound", "Toggle all sound", - toggled => options.setNumberValue("mainvolume", toggled ? 1 : 0), - options.getNumberValue("mainvolume") > 0 - ); - - this.addToggleButton(this.width / 2, 381, "options-option-music", "Toggle music", - toggled => options.setNumberValue("musicvolume", toggled ? 1 : 0), - options.getNumberValue("musicvolume") > 0 - ); - - this.addToggleButton(this.width - 415, 381, "options-option-fullscreen", "Toggle fullscreen", - toggled => options.setBooleanValue("fullscreen", toggled), - options.getBooleanValue("fullscreen") - ); - } - - /** - * Add a toggle button - */ - addToggleButton(x: number, y: number, key: string, tooltip: string, callback: (state: boolean) => boolean, initial: boolean): UIButton { - let button = this.content.button("options-toggle", x, y, undefined, tooltip, state => { - if (callback(state)) { - return state; - } else { - return !state; - } - }, { center: true }); - this.content.in(button).image(key, 0, 0, true); - button.toggle(initial); - return button; - } - - /** - * Show the options menu - */ - pageMenu() { - this.pageCommon(); - - if (this.view.session.primary) { - this.content.button("options-button", this.width / 2, 600, () => this.pageInvite(), "Invite a friend to join this game as spectator", undefined, { - center: true, - text: "Invite a friend", - text_style: { - color: "#9fc4d6", - size: 36, - } - }); - } - - this.content.button("options-button", this.width / 2, 800, () => this.view.gameui.quitGame(), "End the current game and go back to main menu", undefined, { - center: true, - text: "Quit to menu", - text_style: { - color: "#9fc4d6", - size: 36, - } - }); - } - - /** - * Show the credits info - */ - pageCredits() { - this.pageCommon(); - - this.content.text("Credits", this.width / 2, 566, { center: true, size: 48, color: "#dbeff9", shadow: true }); - let credits = "Michaël Lemaire - Code and graphics\n\ + this.content.text("Credits", this.width / 2, 566, { center: true, size: 48, color: "#dbeff9", shadow: true }); + let credits = "Michaël Lemaire - Code and graphics\n\ Viktor Hahn - Ship models\n\ KenneyNL - Sound effects\n\ Matthieu Desprez - Beta testing and ideas\n\ Néstor Delgado - Font\n\ Nicolas Forgo - Ship models\n\ Kevin MacLeod - Musics"; - this.content.text(credits, this.width / 2, 754, { center: true, size: 24, color: "#9fc4d6", shadow: true }); - } + this.content.text(credits, this.width / 2, 754, { center: true, size: 24, color: "#9fc4d6", shadow: true }); + } - /** - * Show the invite page - */ - async pageInvite() { - this.pageCommon(); + /** + * Show the invite page + */ + async pageInvite() { + this.pageCommon(); - let conn = this.view.getConnection(); - try { - let token = await conn.publish(this.view.session, "Multiplayer invitation"); - this.displayMultiplayerToken(token); + let conn = this.view.getConnection(); + try { + let token = await conn.publish(this.view.session, "Multiplayer invitation"); + this.displayMultiplayerToken(token); - if (this.view instanceof BattleView) { - await this.view.multi.setup(this.view, this.view.actual_battle, token, true); - } else { - // TODO - this.displayConnectionError(); - } + if (this.view instanceof BattleView) { + await this.view.multi.setup(this.view, this.view.actual_battle, token, true); + } else { + // TODO + this.displayConnectionError(); + } - this.close(); - } catch (err) { - this.displayConnectionError(); - } - } - - /** - * Display a multiplayer token page - */ - private displayMultiplayerToken(token: string) { - this.pageCommon(); - - this.content.styled({ size: 36 }, content => { - content.text("Give this invite code to your friend:", this.width / 2, 540, { color: "#dbeff9" }); - content.text(token, this.width / 2, 620, { color: "#9FC4D6" }); - content.text("Waiting for a connection...", this.width / 2, 700, { color: "#BF9757" }); - }); - - this.content.button("options-button", this.width / 2, 840, () => this.pageMenu(), "Cancel waiting for the other person to connect", undefined, { - center: true, - text: "Cancel", - text_style: { - color: "#9fc4d6", - size: 36, - } - }); - } - - /** - * Display a connection error - */ - private displayConnectionError() { - this.pageCommon(); - - this.content.text("Could not establish connection to server", this.width / 2, 620, { - color: "#b35b56", - size: 36, - bold: true - }); - - this.content.button("options-button", this.width / 2, 840, () => this.pageMenu(), "Cancel the connection", undefined, { - center: true, - text: "Cancel", - text_style: { - color: "#9fc4d6", - size: 36, - } - }); - } + this.close(); + } catch (err) { + this.displayConnectionError(); } + } + + /** + * Display a multiplayer token page + */ + private displayMultiplayerToken(token: string) { + this.pageCommon(); + + this.content.styled({ size: 36 }, content => { + content.text("Give this invite code to your friend:", this.width / 2, 540, { color: "#dbeff9" }); + content.text(token, this.width / 2, 620, { color: "#9FC4D6" }); + content.text("Waiting for a connection...", this.width / 2, 700, { color: "#BF9757" }); + }); + + this.content.button("options-button", this.width / 2, 840, () => this.pageMenu(), "Cancel waiting for the other person to connect", undefined, { + center: true, + text: "Cancel", + text_style: { + color: "#9fc4d6", + size: 36, + } + }); + } + + /** + * Display a connection error + */ + private displayConnectionError() { + this.pageCommon(); + + this.content.text("Could not establish connection to server", this.width / 2, 620, { + color: "#b35b56", + size: 36, + bold: true + }); + + this.content.button("options-button", this.width / 2, 840, () => this.pageMenu(), "Cancel the connection", undefined, { + center: true, + text: "Cancel", + text_style: { + color: "#9fc4d6", + size: 36, + } + }); + } } diff --git a/tsconfig.json b/tsconfig.json index 3e971f3..4dd3dd8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,21 @@ { - "extends": "./config/common.json", - "include": [ - "src/**/*.ts" + "compilerOptions": { + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "preserveConstEnums": true, + "declaration": true, + "target": "es6", + "lib": [ + "dom", + "es6" ] -} \ No newline at end of file + }, + "exclude": [ + "node_modules", + "dist", + "src/**/*.test.ts" + ] +}