From 9e9bdefdc652aceb660c4a5e72a1028b8d009a45 Mon Sep 17 00:00:00 2001 From: Arkadiusz Czekajski Date: Mon, 27 Jan 2020 01:19:50 +0100 Subject: [PATCH] Major rewrite and team shards game implementation (#4) * typescript upgrade * shards implemented * fixes, improvements, examples * readme updated --- .vscode/settings.json | 4 + README.md | 137 ++++++-- dev.tamper.js | 4 +- examples/README.md | 1 + examples/portalBattle-fast-test.md | 26 ++ examples/teamShards-example-fast-test.md | 29 ++ examples/teamShards-example-real-game.md | 57 ++++ index.d.ts | 19 +- package-lock.json | 49 ++- package.json | 4 +- src/Mininomaly.ts | 155 +++++++++ src/MininomalyPlugin.ts | 215 ------------ src/colors.ts | 144 ++++++++ src/commonInterfaces.ts | 33 ++ src/drawMap.ts | 215 ++++++++---- src/iitcHelpers.ts | 18 + src/index.ts | 56 +++- src/interfaces.ts | 32 -- src/{ => portalBattle}/NoopCommunicator.ts | 3 +- src/portalBattle/PortalBattleGame.ts | 157 +++++++++ src/{ => portalBattle}/TgBotCommunicator.ts | 14 +- src/portalBattle/interfaces.ts | 7 + src/teamShards/ShardsNoopCommunicator.ts | 17 + src/teamShards/ShardsTgBotCommunicator.ts | 161 +++++++++ src/teamShards/TeamShardsGame.ts | 348 ++++++++++++++++++++ src/teamShards/visualizeOnIITC.ts | 59 ++++ src/userscript.meta.js | 10 +- tsconfig.json | 5 + 28 files changed, 1616 insertions(+), 363 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 examples/README.md create mode 100644 examples/portalBattle-fast-test.md create mode 100644 examples/teamShards-example-fast-test.md create mode 100644 examples/teamShards-example-real-game.md create mode 100644 src/Mininomaly.ts delete mode 100644 src/MininomalyPlugin.ts create mode 100644 src/colors.ts create mode 100644 src/commonInterfaces.ts create mode 100644 src/iitcHelpers.ts delete mode 100644 src/interfaces.ts rename src/{ => portalBattle}/NoopCommunicator.ts (87%) create mode 100644 src/portalBattle/PortalBattleGame.ts rename src/{ => portalBattle}/TgBotCommunicator.ts (86%) create mode 100644 src/portalBattle/interfaces.ts create mode 100644 src/teamShards/ShardsNoopCommunicator.ts create mode 100644 src/teamShards/ShardsTgBotCommunicator.ts create mode 100644 src/teamShards/TeamShardsGame.ts create mode 100644 src/teamShards/visualizeOnIITC.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6a196f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib", + "typescript.surveys.enabled": false +} \ No newline at end of file diff --git a/README.md b/README.md index 2429094..8d95bd5 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,143 @@ -# mininomaly-plugin +# iitc-mininomaly-plugin -Provides possibility to run local portal battle event for your Ingress community. Runs in your browser, doesn't perform any additional requests before simply refreshing IITC page to take a measurement, doesn't use external tools, doesn't store data about players. +Provides possibility to run local unofficial anomaly-like events for your Ingress community. + +Key points: +* runs in your browser (no need for servers of any kind) +* doesn't perform any additional requests besides simply refreshing IITC page to take a measurement +* doesn't use external tools (othan than iitc and bookmarks) +* doesn't store data about players ## Implemented mechanics **Portal battle:** You can provide a playbox through the bookmarks. From the playbox, given number of portals will be chosen every X minutes. At the same time the faction ownership of previous set of portals will be checked. This way the raw score for measurement will be counted. The number and interval between measurements can be configured. Final score for a faction is this faction best result achieved through all measurements. Factions results can be also separately multiplied if you choose to provide a bonus (in case one faction is significantly outnumbered). +**Teams shards:** You set a number and names of teams, a playzone middle point, a radius of the playzone and a distance from middle point to targets `R_t`. At given point in time, the start and end positions for each team will be chosen randomly. These will be evenly spaced, all of them at approximately the same `R_t` distance from middle point. Start and end positions for given team are on the opposite side of middle point (making them separated by distance of `2 * R_t`). Every team has their own target and will have their own shards. Every X minutes, the following things will happen for each team: +1. If the team has a "live" shard, it will jump using the randomly chosen link attached to its current portal. It will only jump to the portals it has never visited before. If it cannot jump anywhere, it will jump to one of the the nearest not-visited portals. +2. If in effect of a jump, the shard jumps into its target, it will score a point for the team and disappear. At the same moment the start adn end points of that team are swapped. +3. If the team has no "live" shard, new target is being randomly chosen in the vicinity of their end point and a new shard is spawned close to the start point. +The game continues for the given number of jumps. Score of a team is measured as a number of already scored shards plus `1 - (D_target / D_init)` where `D_target` is a distance from shard to target and `D_init` is a distance from shard spawning point to target. This means that even is the last shard is not scored, the closer a team can move its shard to the target, the better. If a team is unlucky, they may even get some negative partial points from this formula. Factions are not checked in this game, so teams can be cross-faction. Shards cannot be stolen, which means that moving shard of team #1 into target of team #2 will have no effect for points at all and the shard will continue to jump as described. Whole difficulty of this game lies in the fact that the teams' shortest paths cross somewhere in the middle of the playzone. + ## Usage ### Requirements Required tampermonkey plugins are IITC and Bookmarks plugin. Having more plugins than necessary is not advised since it may slow down or interfere with the Mininomaly plugin. Optional: "empty" Telegram bot to send measurements info to given chat. Without bot config, measurements info will be logged to console only and therefore disappear after every iitc refresh. -### Configuration +### Installation +1. Go to the ("Releases")[https://github.com/aczekajski/iitc-plugin-mininomaly/releases] and download the `plugin.user.js` file from the newest release. +2. Install it using your userscripts engine of choice (eg. Tampermonkey) and make sure it runs after the IITC. +3. Make sure you're using a modern browser (newest Chrome/Opera is advised) + +### Running Portal Battle +1. Set `localStorage` values: +```js +localStorage['PRIV.initMininomalyAutomatically'] = 'true'; +localStorage['PRIV.game'] = 'portalBattle'; +// if you're using Telegram bot: +localStorage['PRIV.tgBotToken'] = '123456789:qwertyuiop'; // your private token to tg bot (do not share it with anyone!) +localStorage['PRIV.tgChatId'] = '123456789'; // id of chat where your bot should send the measurement info +``` +2. Refresh tab with iitc +3. Configure your mininomaly, eg. ```js -plugin.miniNomalyPlugin.configureMininomaly( - +new Date('03.09.2019 18:06'), // Timestamp of first measurement. First portals info will be sent an 'measurementInterval' time earlier - 1000*60*15, // measurementInterval - how often will the measurement occur - 4, // how many measurements will be taken - 10, // how many portals will be randomly chosen per measurement - ['my playbox'], // OPTIONAL, defaults to ['*'] - array of bookmarks folders names with playbox portals; special values: 'idOthers' (bookmarked portals that are not in folders), '*' (all bookmarked portals, without exceptions) - { E: 1.4, R: 1 }, // OPTIONAL, defaults to { E: 1, R: 1 } - bonus multiplier for the outnumbered faction. If you set R to 2, points earned by the Resistance will be worth two times as much as points earned by Enlightened +plugin.mininomaly.engine.configureMininomaly( + 0, // currently nothing happens at the game preparation time, so it doesn't matter what time you set here + +new Date('03.09.2019 18:15'), // time of the first measurement. First measurement preparation will happen one interval earlier + 1000*60*15, // measurements interval + 8 // how many measurements will be taken ); ``` +4. Initialize portal battle game specific settings: +```js +plugin.mininomaly.portalBattle.initSettings({ + portalsNumber: 10, // how many portals will be randomly chosen as "ornamented" per measurement + bookmarksFolders: ['city center east', 'city center west']; // OPTIONAL, defaults to ['*'] - array of bookmarks folders names with playbox portals; special values: 'idOthers' (bookmarked portals that are not in folders), '*' (all bookmarked portals, without exceptions) + bonuses: { E: 1.4, R: 1 }, // OPTIONAL, defaults to { E: 1, R: 1 } - bonus multiplier for the outnumbered faction. If you set R to 2, points earned by the Resistance will be worth two times as much as points earned by Enlightened +}); +``` +5. Make sure that whole playbox is visible on the IITC and run: +```js +plugin.mininomaly.engine.runMininomaly(); +``` +6. From now on, everything will happen automatically ;) -### Running +### Running Team Shards 1. Set `localStorage` values: ```js -window.localStorage['PRIV.initMininomalyAutomatically'] = 'true'; +localStorage['PRIV.initMininomalyAutomatically'] = 'true'; +localStorage['PRIV.game'] = 'teamShards'; // if you're using Telegram bot: -window.localStorage['PRIV.tgBotToken'] = '123456789:qwertyuiop'; // your private token to tg bot -window.localStorage['PRIV.tgChatId'] = '123456789'; // id of chat where your bot should send the measurement info +localStorage['PRIV.tgBotToken'] = '123456789:qwertyuiop'; // your private token to tg bot (do not share it with anyone!) +localStorage['PRIV.tgChatId'] = '123456789'; // id of chat where your bot should send the measurement info +``` +2. Refresh tab with iitc +3. Configure your mininomaly, eg. +```js +plugin.mininomaly.engine.configureMininomaly( + +new Date('03.09.2019 17:30'), // at this time, teams start and end positions will be decided and announced to players + +new Date('03.09.2019 18:05'), // time of the first shards jump. First shards are spawned one interval earlier + 1000*60*5, // jumps interval + 24 // how many jumps will happen +); ``` -2. Configure your mininomaly -3. Refresh tab with iitc -4. Run: +4. Initialize team shards game specific settings: ```js -plugin.miniNomalyPlugin.runMininomaly(); +plugin.mininomaly.teamShards.initSettings = ( + { lat: 12.345678, lng: 23.456789 }, // middle point of play zone + 250, // distance in meters from middle point to start and end points (ie. shards spawn points and targets) + 300, // playzone radius, must be greater than previous parameter (shards will never jump outside this area) + ['Suicide Squad', 'we will win', 'nameless'], // array of teams names + 1, // OPTIONAL, defaults to 0. How many jumps the shard should wait on the same portal if there are no links leading to a non-visited portal before jumping randomly. 0 means that it will always immediately jump to a random portal if it cannot jump through a link + ['ac494a4adb174a30b77fd122ad967d8b.16', 'fe89869209c64cb49e99a58acaf7d387.16'] // OPTIONAL, defaults to empty list. This is a list of portal GUIDs that will be excluded from the game, even if they are inside of the playzone - you can exclude inaccessible portals this way. To obtain the portals GUIDs, install "Debug: Raw portal JSON data" plugin (from official IITC site), click on portal and click "Raw Data" under the resonators list +); +``` +5. Make sure that whole playbox is visible on the IITC and run: +```js +plugin.mininomaly.engine.runMininomaly(); ``` +6. From now on, everything will happen automatically ;) ### Stopping during mininomaly -This should be used if you made a mistake. Resuming is not advised. +This should be used if you made a mistake. Resuming will not work, prepare new game instead. +```js +plugin.mininomaly.engine.stopMininomaly(); +``` + +### Telegram bots configuration +Currently there is no way of communicating the results, start and end points, shards positions, ornamented portals to players other than by sending it to the TG chat. You can do it manually (it will be logged to browser console) but it demands you to sit by the computer which runs the game and will generate a delay in a game. Given that, the most convenient way is to create a TG bot and let the browser send the results to the chat automatically. + +### Making sure it works as you wish +It is strongly recommended to test your event before it happens. + +If you're not familiar with the plugin, tinker with it first. Run short events in your browser with few measurements and short intervals and see how it works. Make a chat with just you and the tg bot to see if you can set it properly. In case of teams shards, you can set the `localStorage['DEBUG_visualizeOnDrawTools'] = '1';` to see the game visualisation in the IITC. + +Make a similiar configuration to the final one and run it a day or few days before the event day. You can set the telegram bot to send it already to the chat with real players, so they can see what to expect on the game day. + +Because of unexpected IITC errors that happen from time to time, it is advised to have the DevTools closed while running an event. + +#### How to create a telegram bot: +1. Talk to the `@BotFather` bot on Telegram +2. Follow its instructions on how to create a new bot +3. When you're done, look for the "Use this token to access the HTTP API:" line. Below, there will be a token that you need in `localStorage['PRIV.tgBotToken']`. Remember to not share it with anyone! + +#### How to make a bot send messages to the right chat +1. Invite your newly created bot to the chat you want your communication to be sent +2. Send `/start` to the `@getidsbot` bot +3. Forward any message from the chat created in point 1 to the `@getidsbot` +4. The bot will reply with the message details and you will find a chat id in "Origin chat" section. Note: do not miss the `-` at the begging if it is there! You need to set this id in `localStorage['PRIV.tgChatId']` + +#### How to show a streets on the images sent by tg bot +Unfortunately, for this moment there is no way to use a real maps (if you're a developer and know how to do it, the help will be appreciated!). If you want your players to see the streets (you do want it! šŸ˜…), there is a manual way to do it. Before you configure your mininomaly: +1. Draw the streets of a playbox using IITC drawtools plugin (only polylines, no polygons and no circles) +2. In "DrawTools Opt" choose "Copy Drawn Items" and copy it to clipboard +3. In console, type: ```js -plugin.miniNomalyPlugin.stopMininomaly(); +localStorage['PRIV.mapLines'] = `HERE_GOES_THE_CLIPBOARD`; ``` +replacing `HERE_GOES_THE_CLIPBOARD` with the previously copied drawn items. + +### Questions? +If anything is unclear, do not hesitate to open an issue with question šŸ˜‰ ## Development of plugin 1. Install packages (run `npm i`) @@ -52,7 +147,7 @@ plugin.miniNomalyPlugin.stopMininomaly(); Sourcemaps are being properly emitted so you can debug script in chrome seeing an original source (see `webpack://` in Sources tab; if you can't see it, refresh page with devtools open). -## Building plugin +### Building plugin Run `npm run build` task, your plugin will be built into `dist` directory. If you set env variable `NODE_ENV` to `production`, the build will be minified and will not containt source maps. diff --git a/dev.tamper.js b/dev.tamper.js index 92a6ca0..3cc5485 100644 --- a/dev.tamper.js +++ b/dev.tamper.js @@ -5,8 +5,8 @@ // @version 1.0 // @namespace https://github.com/jonatkins/ingress-intel-total-conversion // @description Do stuff -// @include https://*.ingress.com/intel* -// @include http://*.ingress.com/intel* +// @match https://intel.ingress.com/* +// @match http://intel.ingress.com/* // @match https://*.ingress.com/intel* // @match http://*.ingress.com/intel* // @grant none diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4e31f13 --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +In this folder, there are various examples on how to run your game. Check the *.md files around here diff --git a/examples/portalBattle-fast-test.md b/examples/portalBattle-fast-test.md new file mode 100644 index 0000000..25fd36b --- /dev/null +++ b/examples/portalBattle-fast-test.md @@ -0,0 +1,26 @@ +To see the game in IITC: +1. Make sure you have bookmarks plugin installed +2. Import the following bookmarks: +``` +{"maps":{"idOthers":{"label":"Others","state":1,"bkmrk":{}}},"portals":{"idOthers":{"label":"Others","state":1,"bkmrk":{"id1580081505439058":{"guid":"53e304e949e9494d854c500107deca2e.16","latlng":"35.66919,139.701563","label":"Yoyogi Koen Stone"},"id1580081507103196":{"guid":"beb192ddede54562ae0fdb7b927322b5.16","latlng":"35.668963,139.701302","label":"恄恔悇恆ć‚æć‚¤ćƒ«ēµµ"},"id1580081508275298":{"guid":"518d038203554ab78b97117b0101c0bf.16","latlng":"35.668857,139.701035","label":"ꤍē‰©ć«ćŖć£ćŸē™½ē·š ä»£ć€…ęœØ公園"},"id1580081509335368":{"guid":"7cd3cba7018548e7b22680b81c245a24.16","latlng":"35.668786,139.700684","label":"čŠ±ć®å°č·Æ"},"id1580081510732416":{"guid":"c6d268afa94e41a1913766e400eaedc1.16","latlng":"35.669007,139.700896","label":"ä»£ć€…ęœØ公園"},"id1580081511602535":{"guid":"84123877ba6748e49689383c1924b6ff.16","latlng":"35.669106,139.701099","label":"ćƒ©ć‚±ćƒƒćƒˆć‚æć‚¤ćƒ«"},"id158008151235463":{"guid":"69e3114785a044f79b2fa2b2558a4fc0.16","latlng":"35.669305,139.70125","label":"åŗƒå “ć®ć‚¢ćƒ¼ćƒˆ"},"id1580081514559781":{"guid":"519b3eedfb2c417b86eda90f413f403d.16","latlng":"35.670182,139.700194","label":"ć‚ŖćƒŖćƒ³ćƒ”ćƒƒć‚Æčؘåæµå®æ舎ćØč¦‹ęœ¬åœ’"},"id1580081515952882":{"guid":"8b0e1818a2fe406c9415fb564ebd95f9.16","latlng":"35.670339,139.69989","label":"国土ē·‘化運動čؘåæµē¢‘"},"id1580081517016954":{"guid":"f0f40fecfd824c529b7252aac46fe6fb.16","latlng":"35.670251,139.699545","label":"ć—ć‚ć‚ć›ć®åƒ"},"id15800815184401090":{"guid":"3dcc7cfd9a59416cb8c98ec6ac1f6d1a.16","latlng":"35.669588,139.699691","label":"Friendship Blossoms"},"id1580081519179113":{"guid":"5495d968d691412f832b80e34f1fb593.16","latlng":"35.669129,139.699949","label":"ä»£ć€…ęœØ公園ę²æ革"},"id15800815199431284":{"guid":"de4a1ca4513e49fc85f77539ec9a38e9.16","latlng":"35.668997,139.699757","label":"Parks"},"id15800815213201370":{"guid":"d39162c95beb43b582384ff9330e9b79.16","latlng":"35.66943,139.699323","label":"悰ćƒŖćƒ¼ćƒ³ć‚¢ćƒ‰ćƒ™ćƒ³ćƒćƒ£ćƒ¼"},"id15800815225351438":{"guid":"ac11a7c098064a02a42b2bcf65be3c1e.16","latlng":"35.669113,139.699017","label":"ä»£ć€…ęœØå…¬åœ’ć€€čŠ±ć®å°å¾„"},"id15800815236071585":{"guid":"b4598b1532c147cb8116e0031dd16175.16","latlng":"35.668593,139.698789","label":"銀ꝏć‚æć‚¤ćƒ«"},"id15800815245611649":{"guid":"f81f891648bd4abe92481e0d8fbe2c0a.16","latlng":"35.668543,139.698144","label":"ä»£ć€…ęœØå…¬åœ’ę­©é“äøŠć®ę•·ēŸ³ęØ”ę§˜"},"id15800815258191760":{"guid":"909489943509452092eb7031acafd5ec.16","latlng":"35.668492,139.696762","label":"Ꙃč؈台"},"id15800815281061833":{"guid":"0335c3dfdb65435c822a7774e996f9a6.16","latlng":"35.668928,139.696557","label":"Red Dragon Wall Art"},"id15800815290031966":{"guid":"ad2b7fa992d2433aaf6004c8c0518f2f.16","latlng":"35.669128,139.696396","label":"ä»£ć€…ęœØ公園ęø‹č°·é–€"},"id15800815298812076":{"guid":"0da1daf4287b4a2e8907222693214dee.16","latlng":"35.669325,139.696331","label":"čŠ±ć®å°å¾„"},"id15800815308182183":{"guid":"bb5d60b00d7747f7a6f6eb6c6c55d530.16","latlng":"35.669288,139.696096","label":"ę—„ęœ¬čˆŖē©ŗē™ŗ始之地čؘåæµē¢‘"},"id15800815319052276":{"guid":"4d979a5845e54f14ae0fc72c52e5db89.16","latlng":"35.669359,139.696834","label":"ćƒćƒ©ć®åœ’ (ćƒćƒ©ć‚¢ćƒ¼ćƒ) Rose Garden"},"id15800815350182376":{"guid":"b594a27a76204f13ac30a5d16b0229fc.16","latlng":"35.670252,139.69919","label":"ć‚±ćƒ„ć‚”ćƒ«ć‚³ć‚¢ćƒˆćƒ«"},"id15800815361582480":{"guid":"47f3cb84bbfc4ab7a179f506358b8163.16","latlng":"35.670266,139.698774","label":"Pine Tree of Imperial Troop Review"},"id15800815373562596":{"guid":"5940b28a43c446029e19cb7066d596b1.16","latlng":"35.669855,139.69848","label":"Yoyogi Koen Memorial"},"id15800815384992611":{"guid":"3697e786a4f34aaca8adac12617abb8e.16","latlng":"35.670739,139.698133","label":"ćƒćƒ¼ćƒ‰ ć‚µćƒ³ć‚Æćƒćƒ„ć‚¢ćƒŖ"},"id15800815394422793":{"guid":"0892477cf2914549abb6ce8225191ceb.16","latlng":"35.670681,139.697797","label":"ä»£ć€…ęœØ公園 ä¼‘ę†©čˆŽ"},"id15800815409712865":{"guid":"b50a0863b08b438499798d8ab099adc2.16","latlng":"35.671252,139.697556","label":"ē“”ē™½ć‚µćƒ³ć‚Æćƒćƒ„ć‚¢ćƒŖćƒ¼"},"id15800815419032931":{"guid":"9db7f282c59044b0846bac900fac062d.16","latlng":"35.671575,139.69707","label":"Yoyogi Koen Map"},"id15800815428773059":{"guid":"94c33736658e45cb8f44a9bf1ee9f56d.16","latlng":"35.671619,139.696509","label":"ćƒˆć‚¤ćƒ¬"},"id15800815446753110":{"guid":"679a65762ffd4a4c91f00da18333557c.16","latlng":"35.670739,139.696868","label":"ä»£ć€…ęœØ公園 恩悓恐悊恮ēخ锞"},"id15800815469143297":{"guid":"bd4451408dd543d1860c709d9df1fd20.16","latlng":"35.671764,139.695928","label":"徔休ćæ処"},"id15800815476193342":{"guid":"61ae66d235f0452f89cc2b942dd8dfcb.16","latlng":"35.672249,139.695565","label":"ä¼‘ę†©ę‰€"},"id15800815485633453":{"guid":"3c8a23e3ce444c4db518272a713ab9d4.16","latlng":"35.672463,139.696099","label":"ęعęœØē”ŸęÆåŒŗ域"},"id15800815501803552":{"guid":"9cb189bb9b37455b9f0f8c6f2b77ea6b.16","latlng":"35.672702,139.695682","label":"ä»£ć€…ęœØå…¬åœ’ćƒćƒ¼ćƒ–ć‚¬ćƒ¼ćƒ‡ćƒ³"},"id15800815510613673":{"guid":"9c7e5e421210495cb0e571b77b7df9fc.16","latlng":"35.672882,139.695296","label":"Stone Tables"},"id15800815550633778":{"guid":"62b0805866ab4b9b8754d61dde0c2572.16","latlng":"35.672816,139.695185","label":"ä»£ć€…ęœØå…¬åœ’ę—„ę™‚č؈"},"id15800815575013846":{"guid":"abf7bb75b86b44f6a9c4dcff7096b595.16","latlng":"35.673273,139.69423","label":"ē«œēˆŖ꧐"},"id1580081558719391":{"guid":"2191e346cf774647935dfd12c565496a.16","latlng":"35.673454,139.695025","label":"Pave for Wheelchair"},"id15800815595794051":{"guid":"005ae1ad49ee4e4d820f32eaff0905f2.16","latlng":"35.672673,139.69459","label":"ä»£ć€…ęœØå…¬åœ’ćƒ‰ćƒƒć‚°ćƒ©ćƒ³"},"id15800815603254112":{"guid":"45afa941e65d480ca0c60c3036eba977.16","latlng":"35.672576,139.694262","label":"ä»£ć€…ęœØå…¬åœ’ćƒ‰ćƒƒć‚°ćƒ©ćƒ³"},"id15800815612854231":{"guid":"04bfb1f0691041ea97b4cf1285844221.16","latlng":"35.672498,139.694012","label":"ä¼‘ę†©ę‰€"},"id15800815621844368":{"guid":"80f59a119e29414bb1450dd8f013342c.16","latlng":"35.672408,139.694591","label":"Time"},"id15800815641014433":{"guid":"cda23f3d28b24e46993cd0918b89f5b7.16","latlng":"35.673061,139.693771","label":"ćƒˆć‚¤ćƒ¬"},"id15800815658444524":{"guid":"f3b347c6f19e446f8c9a534104eff3e9.16","latlng":"35.672183,139.692598","label":"ę°“é£²ćæå “"},"id15800815668614645":{"guid":"71b40d3a2040455b8f7943a299399274.16","latlng":"35.672146,139.691816","label":"ä»£ć€…ęœØå…¬åœ’ćƒžćƒƒćƒ—"},"id15800815678844766":{"guid":"62f085e9e9244e44a805c04e5f0a6d98.16","latlng":"35.672205,139.69134","label":"ä»£ć€…ęœØå…¬åœ’å‚å®®ę©‹é–€"},"id15800815692854892":{"guid":"c65ce0a4e2ac4c0a935c52f243cdc2ee.16","latlng":"35.671787,139.691645","label":"ä»£ć€…ęœØå…¬åœ’ć‚µćƒ¼ćƒ“ć‚¹ć‚»ćƒ³ć‚æćƒ¼"},"id15800815711854998":{"guid":"1fa56a459bd04625b121c4284e48b93a.16","latlng":"35.671659,139.693044","label":"恩悓恐悊恮ēخ锞"},"id15800815720995080":{"guid":"f752a4c6ce8645d09870f4e4322b5b44.16","latlng":"35.671863,139.693152","label":"č¦‹ćˆćŖ恄č²Æę°“ę± "},"id1580081573063518":{"guid":"98a2656b59dd45c9ab151e040845473b.16","latlng":"35.67169,139.692714","label":"十四ēƒˆå£«č‡Ŗåˆƒć®åŗ­"},"id15800815748745244":{"guid":"40ba34f0eee84a8191755e0cf2410579.16","latlng":"35.670512,139.691088","label":"ä»£ć€…ęœØ公園č„æ門"},"id1580081576536531":{"guid":"0ca11ffb7ddf44578167abb05132e7eb.16","latlng":"35.670189,139.693112","label":"ć‚µć‚¤ć‚«ćƒć®ęœØ"},"id15800815785795414":{"guid":"3ce2d78146db4d929a86c5597a18d6ce.16","latlng":"35.670125,139.694269","label":"Cherry Blossom Watering Sign"},"id15800815797645585":{"guid":"66890a0ee6de4dbc82e270de1fafed58.16","latlng":"35.669528,139.693965","label":"ę—„ęœ¬čˆŖē©ŗē™ŗ始之地čؘåæµ"},"id15800815808715635":{"guid":"099bf1f104644476afc9e74f32cbdca9.16","latlng":"35.669115,139.693822","label":"ä»£ć€…ęœØ公園 南門"},"id1580081582104579":{"guid":"f69b960011ce472298f17d093e3c671e.16","latlng":"35.668713,139.692943","label":"ę—„ęœ¬åˆé£›č”Œć®åœ°"},"id15800815837395887":{"guid":"d447be827bd4454ca430faf479cbba14.16","latlng":"35.668622,139.693182","label":"ꗄ野ē†Šč—ä¹‹åƒ, å¾·å·å„½ę•ä¹‹åƒ"},"id15800815865455990":{"guid":"46fa27a5ed4e435fb95d2192d82823f7.16","latlng":"35.668419,139.694586","label":"Sakura Plate - ę”œć®ćƒ—ćƒ¬ćƒ¼ćƒˆ"},"id15800815882876032":{"guid":"45703952ad90463dbbbff677ff7922cf.16","latlng":"35.669024,139.69412","label":"Yoyogi Park Information Sign"},"id15800815894996188":{"guid":"40045cb281dc479497af061c513f13b7.16","latlng":"35.669304,139.695261","label":"Yoyogi Garden Path"},"id15800815957246228":{"guid":"53ad6e736e234a44b568163e7721b864.16","latlng":"35.669101,139.700558","label":"ä»£ć€…ęœØ公園ćØć‘ć„å””"},"id1580081766143089":{"guid":"cd571569567a4d78a5a57e4673ee3f07.16","latlng":"35.671122,139.694816","label":"é’ć„ęŸ±"},"id1580081767884148":{"guid":"8164b7bf25e8462b8516acf96e496496.11","latlng":"35.670457,139.695645","label":"Yoyogi Park"},"id1580081769756257":{"guid":"320d796c3be5459c9e08e246577c3a5b.16","latlng":"35.669772,139.696079","label":"å™“ę°“"},"id1580081771303342":{"guid":"169b7d11340a4c85b9a2908a9cd661d1.16","latlng":"35.669412,139.695746","label":"č¦‹ćˆćŖ恄č²Æę°“ę± "}}}},"drawtools":{"idOthers":{"label":"Others","state":1,"bkmrk":{}}}} +``` +3. Go to the following portal: https://intel.ingress.com/intel?ll=35.671122,139.694816&z=17&pll=35.671122,139.694816 +4. Adjust zoom level to see all bookmarked portals (but still have the all portals zoom) +5. Go to https://intel.ingress.com/intel (to remove params from URL) +6. Configure the game according to the main instruction + +Configuration (points 3-5 from the main instruction): +```js +const start = Math.ceil((+new Date() + 30000)/60000)*60000; +const inter = 1000*60*1; + +plugin.mininomaly.portalBattle.initSettings({ + portalsNumber: 10, + bonuses: { E: 1, R: 2 }, +}); +plugin.mininomaly.engine.configureMininomaly(0, start + inter, inter, 5); + +plugin.mininomaly.engine.runMininomaly(); +``` + +It will play the fast test game. First ornaments will be sent to players at the beginning of the closest minute (ie. when seconds hit 00) which is not closer than 30 seconds. It will randomly choose 10 portals for ornaments out of all bookmarks every one minute and take measurement for the previous set of ornaments. Resistance points will be worth twice as much as Enlightened points when calculating the total results. diff --git a/examples/teamShards-example-fast-test.md b/examples/teamShards-example-fast-test.md new file mode 100644 index 0000000..d6c265b --- /dev/null +++ b/examples/teamShards-example-fast-test.md @@ -0,0 +1,29 @@ + +To see the game in IITC: +1. Make sure you have DrawTools installed. +2. Import the following draw: +``` +[{"type":"circle","latLng":{"lat":35.69435392283978,"lng":139.76568460464478},"radius":486.0618444493037,"color":"#a24ac3"}] +``` +3. Go to the following portal: https://intel.ingress.com/intel?ll=35.694807,139.765495&z=17&pll=35.694807,139.765495 +4. Adjust zoom level to see the whole purple circle (but still have the all portals zoom) +5. Go to https://intel.ingress.com/intel (to remove params from URL) +6. Configure the game according to the main instruction + +Configuration (points 3-5 from the main instruction): +```js +localStorage['DEBUG_visualizeOnDrawTools'] = '1'; + +const start = Math.ceil(+new Date()/60000)*60000; +const inter = 1000*60*1; +const teams = ['one', 'two', 'three', 'four', 'five', 'six']; + +plugin.mininomaly.teamShards.initSettings({ lat: 35.69435392283978, lng: 139.76568460464478 }, 420, 486, teams, 1, ['b4015977539449cfbd0cfe59846286e4.16']); +plugin.mininomaly.engine.configureMininomaly(start, start + 2 * inter, inter, 5); + +plugin.mininomaly.engine.runMininomaly(); +``` + +This will play a very fast test. It will send the teams start points at the beginning of nearest minute (ie. when seconds on a clock hit 00). One minute later the first shards will spawn. From this moment, every one minute, a jump will occur. 5 jumps will happen. + +One portal is excluded because it's across the big street. If shard cannot jump by the link, it will wait for one jump before making a random jump. diff --git a/examples/teamShards-example-real-game.md b/examples/teamShards-example-real-game.md new file mode 100644 index 0000000..66b98c0 --- /dev/null +++ b/examples/teamShards-example-real-game.md @@ -0,0 +1,57 @@ + +To see the game in IITC: +1. Make sure you have DrawTools installed. +2. Import the following draw: +``` +[{"type":"circle","latLng":{"lat":50.0625250172566,"lng":19.93896245956421},"radius":412.5,"color":"#a24ac3"}] +``` +3. Go to the following portal: https://intel.ingress.com/intel?ll=50.062276,19.938786&z=17&pll=50.062276,19.938786 +4. Adjust zoom level to see the whole purple circle (but still have the all portals zoom) +5. Go to https://intel.ingress.com/intel (to remove params from URL) +6. Configure the game according to the main instruction + +Configuration (points 3-5 from the main instruction): +```js +const preparation = +new Date('01.02.2020 12:45'); +const start = +new Date('01.02.2020 13:00'); +const inter = 1000*60*4; +const teams = ['one', + 'two', + 'three', + 'four', + 'five', + 'six', +].sort(() => Math.random()-0.5); +const middle = { lat: 50.0625250172566, lng: 19.93896245956421 }; +const exclude = ['ac494a4adb174a30b77fd122ad967d8b.16', 'fe89869209c64cb49e99a58acaf7d387.16']; + +plugin.mininomaly.teamShards.initSettings(middle, 330, 412.5, teams, 1, exclude); +plugin.mininomaly.engine.configureMininomaly(preparation, start + inter, inter, 15); + +plugin.mininomaly.engine.runMininomaly(); +``` + +This is a game that will happen on January 1st 2020. + +Timeline: +| time | event | +| ----- | ----------------------------------------- | +| 12:45 | The start positions will be sent to teams | +| 13:00 | The first shards will spawn | +| 13:04 | jump #1 | +| 13:08 | jump #2 | +| 13:12 | jump #3 | +| 13:16 | jump #4 | +| 13:20 | jump #5 | +| 13:24 | jump #6 | +| 13:28 | jump #7 | +| 13:32 | jump #8 | +| 13:36 | jump #9 | +| 13:40 | jump #10 | +| 13:44 | jump #11 | +| 13:48 | jump #12 | +| 13:52 | jump #13 | +| 13:56 | jump #14 | +| 14:00 | jump #15 and final score announcement | + +Two portals are excluded because they're hard to access. If shard cannot jump by the link, it will wait for one jump before making a random jump. diff --git a/index.d.ts b/index.d.ts index da4d5c9..4211faa 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,17 @@ declare function dialog(config: {}): void; declare var L: any; +interface PortalData { + health: number; + image: string; + level: number; + latE6: number; + lngE6: number; + resCount: number; + team: 'R' | 'E' | 'N'; + title: string; +} + interface Window { plugin: any; addHook: (hook: string, callback: () => void) => void; @@ -16,7 +27,13 @@ interface Window { bootPlugins: Array; iitcLoaded: boolean; map: any; // TODO: prepare better interface - portals: { [key: string]: any }; // TODO: prepare better interface + portals: { + [key: string]: { + _latlng?: { lat: number; lng: number; }; + options?: { data?: PortalData; guid: string; }; + } + }; // TODO: prepare better interface + links: { [key: string]: any }; // TODO: prepare better interface selectedPortal: any; // TODO: prepare better interface ornaments: any; // TODO: prepare better interface artifact: any; // TODO: prepare better interface diff --git a/package-lock.json b/package-lock.json index e5d397c..e3ad79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "iitc-plugin-mininomaly", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2989,7 +2989,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3010,12 +3011,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" @@ -3030,17 +3033,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", @@ -3157,7 +3163,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3169,6 +3176,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3183,6 +3191,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3190,12 +3199,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3214,6 +3225,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3294,7 +3306,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3306,6 +3319,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3391,7 +3405,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3427,6 +3442,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", @@ -3446,6 +3462,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3489,12 +3506,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -6800,9 +6819,9 @@ "dev": true }, "typescript": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", - "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==", + "version": "3.3.4000", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz", + "integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==", "dev": true }, "unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index 781c5f3..31ab379 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iitc-plugin-mininomaly", - "version": "1.0.0", + "version": "2.0.0", "description": "", "main": "dist/plugin.user.js", "scripts": { @@ -17,7 +17,7 @@ "copy-webpack-plugin": "^4.5.2", "fork-ts-checker-webpack-plugin": "^0.4.8", "ts-loader": "^4.4.2", - "typescript": "^3.0.1", + "typescript": "^3.3.4000", "webpack": "^4.29.6", "webpack-cli": "^3.2.3", "webpack-serve": "^2.0.3" diff --git a/src/Mininomaly.ts b/src/Mininomaly.ts new file mode 100644 index 0000000..116e49e --- /dev/null +++ b/src/Mininomaly.ts @@ -0,0 +1,155 @@ +import { MininomalyGame, MininomalyEventSettings } from "./commonInterfaces"; + +interface MininomalySettings { + game: MininomalyGame; +} + +export default class Mininomaly { + game: MininomalyGame; + + constructor({ game }: MininomalySettings) { + this.game = game; + + if (+localStorage['mininomaly.running']) { + console.log('mininomaly: WAITING FOR BOOKMARKS'); + const waitTimer = setInterval(() => { + // wait for bookmarks and then launch a timer + // maybe there's no reason for waiting, since they should just exist in local storage + const bkm = window.plugin && window.plugin.bookmarks && window.plugin.bookmarks.bkmrksObj && window.plugin.bookmarks.bkmrksObj.portals; + if (bkm) { + clearInterval(waitTimer); + + if (localStorage['mininomaly.serializedGame']) { + this.game.deserialize(localStorage['mininomaly.serializedGame']); + } + + this.runMininomalyTimer(); + } + }, 500); + } + } + + configureMininomaly = ( + preparationTime: number, + firstMeasurementTime: number, + measurementInterval: number, + numberOfMeasurements: number, + ) => { + if (preparationTime && preparationTime >= (firstMeasurementTime - measurementInterval)) { + console.error('Not configured. Preparation time cannot be after first measurement minus measurement interval'); + } + localStorage['mininomaly.settings'] = JSON.stringify({ + preparationTime, + firstMeasurementTime, + measurementInterval, + numberOfMeasurements, + } as MininomalyEventSettings); + } + + runMininomaly = () => { + const settings: MininomalyEventSettings = this.getOptions(); + if (settings + && typeof settings.firstMeasurementTime === 'number' + && settings.firstMeasurementTime > +new Date() + && typeof settings.preparationTime === 'number' + ) { + if (!+localStorage['mininomaly.running']) { + localStorage['mininomaly.running'] = '1'; + localStorage['mininomaly.nextMeasurement'] = '-1'; + localStorage['mininomaly.phase'] = 'waiting'; + localStorage['mininomaly.serializedGame'] = ''; + localStorage['mininomaly.prepared'] = '0'; + this.runMininomalyTimer(); + } else { + console.error('Mininomaly already running!'); + } + } else { + console.warn('No mininomaly settings found or mininomaly expired'); + } + } + + private mininomalyTimer: NodeJS.Timer; + private runMininomalyTimer = () => { + this.mininomalyTimer = setInterval(this.executeMininomalyPhase, 1000); + this.executeMininomalyPhase(); + }; + + private executeMininomalyPhase = () => { + const eventSettings: MininomalyEventSettings = this.getOptions(); + const currentMeasurement = +localStorage['mininomaly.nextMeasurement']; + const currentMeasurementTime = eventSettings.firstMeasurementTime + currentMeasurement * eventSettings.measurementInterval; + const isPrepared = !!+localStorage['mininomaly.prepared']; + + console.log('mininomaly phase:', localStorage['mininomaly.phase']); + + switch (localStorage['mininomaly.phase']) { + case 'waiting': + if (isPrepared) { + if (+new Date() >= currentMeasurementTime) { + if (currentMeasurement >= 0) { + clearInterval(this.mininomalyTimer); + localStorage['mininomaly.phase'] = 'takeMeasurement'; + localStorage['mininomaly.serializedGame'] = this.game.serialize(); + window.location.reload(); + } else { + this.prepareNextMeasurement(-1, currentMeasurementTime + eventSettings.measurementInterval, eventSettings); + } + } + } else { + const preparationTime = eventSettings.preparationTime; + if (+new Date() >= preparationTime) { + clearInterval(this.mininomalyTimer); + localStorage['mininomaly.phase'] = 'prepare'; + localStorage['mininomaly.serializedGame'] = this.game.serialize(); + window.location.reload(); + } + } + break; + case 'takeMeasurement': + clearInterval(this.mininomalyTimer); + + const mapLoadedCallback = () => { + window.removeHook('mapDataRefreshEnd', mapLoadedCallback); + this.game.takeMeasurement(currentMeasurement, currentMeasurementTime, eventSettings); + } + + console.log('mininomaly: WAITING FOR MAP LOAD'); + window.addHook('mapDataRefreshEnd', mapLoadedCallback); + + this.prepareNextMeasurement(currentMeasurement, currentMeasurementTime + eventSettings.measurementInterval, eventSettings); + + this.runMininomalyTimer(); + break; + case 'prepare': + localStorage['mininomaly.prepared'] = '1'; + localStorage['mininomaly.phase'] = 'waiting'; + + this.game.prepareGame(eventSettings); + break; + case 'end': + this.stopMininomaly(); + break; + } + } + + private prepareNextMeasurement = (currentMeasurement: number, currentMeasurementTime: number, eventSettings: MininomalyEventSettings) => { + if (currentMeasurement + 1 < eventSettings.numberOfMeasurements) { + localStorage['mininomaly.nextMeasurement'] = (currentMeasurement + 1).toString(); + localStorage['mininomaly.phase'] = 'waiting'; + + this.game.prepareNextMeasurement(currentMeasurement + 1, currentMeasurementTime, eventSettings); + } else { + localStorage['mininomaly.phase'] = 'end'; + } + } + + stopMininomaly = () => { + clearInterval(this.mininomalyTimer); + localStorage['mininomaly.running'] = '0'; + + this.game.end(); + } + + getOptions = (): MininomalyEventSettings => JSON.parse(localStorage['mininomaly.settings']); + +} diff --git a/src/MininomalyPlugin.ts b/src/MininomalyPlugin.ts deleted file mode 100644 index ddfd424..0000000 --- a/src/MininomalyPlugin.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { MiniNomalySettings, MeasurementData, BookmarksPortalInfo, BotCommunicator, MeasurementPoints } from "./interfaces"; - -export default class MiniNomalyPlugin { - botCommunicator: BotCommunicator; - - constructor({ botCommunicator }: { botCommunicator: BotCommunicator }) { - this.botCommunicator = botCommunicator; - - if (+localStorage['mininomaly.running']) { - console.log('mininomaly: WAITING FOR BOOKMARKS'); - // window.addHook('iitcLoaded', this.runMininomalyTimer); - const waitTimer = setInterval(() => { - const portals = this.getAllPlayboxPortals(); - if (portals && portals.length > 0) { - clearInterval(waitTimer); - this.runMininomalyTimer(); - } - }, 500); - } - } - - configureMininomaly = ( - firstMeasurementTime: number, - measurementInterval: number, - numberOfMeasurements: number, - portalsNumber: number, - bookmarksFolders: string[] = ['*'], - bonuses: MeasurementPoints = { E: 1, R: 1 } - ) => { - localStorage['mininomaly.settings'] = JSON.stringify({ - firstMeasurementTime, - measurementInterval, - numberOfMeasurements, - portalsNumber, - bookmarksFolders, - bonuses - } as MiniNomalySettings); - } - - runMininomaly = () => { - const settings: MiniNomalySettings = this.getOptions(); - if (settings && settings.firstMeasurementTime > +new Date()) { - localStorage['mininomaly.running'] = '1'; - localStorage['mininomaly.measurements'] = JSON.stringify([]); - localStorage['mininomaly.nextMeasurement'] = '-1'; - localStorage['mininomaly.phase'] = 'waiting'; - this.runMininomalyTimer(); - } else { - console.warn('No mininomaly settings found or mininomaly expired'); - } - } - - private mininomalyTimer: NodeJS.Timer; - private runMininomalyTimer = () => { - this.mininomalyTimer = setInterval(this.executeMininomalyPhase, 1000); - this.executeMininomalyPhase(); - }; - - private executeMininomalyPhase = () => { - const currentMeasurement = +localStorage['mininomaly.nextMeasurement']; - const options: MiniNomalySettings = this.getOptions(); - console.log('mininomaly phase:', localStorage['mininomaly.phase']); - switch (localStorage['mininomaly.phase']) { - case 'waiting': - const measurementTime = options.firstMeasurementTime + currentMeasurement * options.measurementInterval; - if (+new Date() >= measurementTime) { - if (currentMeasurement >= 0) { - clearInterval(this.mininomalyTimer); - localStorage['mininomaly.phase'] = 'takeMeasurement'; - window.location.reload(); - } else { - // choose portals for first measurement - this.prepareNextMeasurement(-1, options, []); - } - } - break; - case 'takeMeasurement': - clearInterval(this.mininomalyTimer); - const measurements: MeasurementData[] = JSON.parse(localStorage['mininomaly.measurements']); - const mapLoadedCallback = () => { - window.removeHook('mapDataRefreshEnd', mapLoadedCallback); - - const measurementResult = this.takeMeasurement(measurements[currentMeasurement].portals); - console.log(measurementResult); - measurements[currentMeasurement].points = measurementResult.points; - measurements[currentMeasurement].details = measurementResult.details; - - // save to LS - localStorage['mininomaly.measurements'] = JSON.stringify(measurements); - - // bot communication about current measurement results - const total = { E: 0, R: 0 }; - for (const m of measurements) { - if (m.points) { - if (m.points.E > total.E) { - total.E = m.points.E * options.bonuses.E; - } - if (m.points.R > total.R) { - total.R = m.points.R * options.bonuses.R; - } - } - } - this.botCommunicator.sendMeasurementResult(currentMeasurement, options.numberOfMeasurements, measurementResult.points, total); - } - console.log('mininomaly: WAITING FOR MAP LOAD'); - window.addHook('mapDataRefreshEnd', mapLoadedCallback); - - this.prepareNextMeasurement(currentMeasurement, options, measurements); - - this.runMininomalyTimer(); - break; - case 'end': - this.stopMininomaly(); - // summarize or STH - break; - } - } - - private prepareNextMeasurement = (currentMeasurement: number, options: MiniNomalySettings, measurements: MeasurementData[]) => { - if (currentMeasurement + 1 < options.numberOfMeasurements) { - const nextMeasurement = this.chooseRandomPortals(options.portalsNumber); - measurements.push({ - portals: nextMeasurement.map((p) => p.guid), - points: null, - details: null, - }); - localStorage['mininomaly.nextMeasurement'] = (currentMeasurement + 1).toString(); - localStorage['mininomaly.measurements'] = JSON.stringify(measurements); - localStorage['mininomaly.phase'] = 'waiting'; - - // bot communication about next measurement - this.botCommunicator.sendNextMeasurementList( - currentMeasurement + 1, - options.firstMeasurementTime + (currentMeasurement + 1) * options.measurementInterval, - nextMeasurement - ); - this.botCommunicator.sendNextMeasurementImage( - this.getAllPlayboxPortals(), - nextMeasurement, - this.getAllPlayboxPortals(), - currentMeasurement + 1, - options.firstMeasurementTime + (currentMeasurement + 1) * options.measurementInterval, - ); - } else { - localStorage['mininomaly.phase'] = 'end'; - } - } - - stopMininomaly = () => { - clearInterval(this.mininomalyTimer); - localStorage['mininomaly.running'] = '0'; - } - - getOptions = (): MiniNomalySettings => JSON.parse(localStorage['mininomaly.settings']); - - getAllPlayboxPortals = () => { - const bkm = window.plugin && window.plugin.bookmarks && window.plugin.bookmarks.bkmrksObj && window.plugin.bookmarks.bkmrksObj.portals; - - if (!bkm) return; - - const options = this.getOptions(); - - const all = []; - for (const folderId in bkm) { - const folder = bkm[folderId]; - const folderName = folder.label; - if (options.bookmarksFolders.indexOf(folderName) >= 0 || options.bookmarksFolders.indexOf(folderId) >= 0 || options.bookmarksFolders.indexOf('*') >= 0) { - for (const portalId in folder.bkmrk) { - const portal = folder.bkmrk[portalId]; - - all.push(portal); - } - } - } - - return all; - } - - chooseRandomPortals = (howMany: number): BookmarksPortalInfo[] => { - const all = this.getAllPlayboxPortals(); - - const chosen = []; - for (let i = 0; i < howMany; ++i) { - let index = Math.floor(Math.random() * all.length); - if (index === all.length) { - index = all.length - 1; - } - - chosen.push(all.splice(index, 1)[0]); - } - - return chosen; - } - - takeMeasurement = (portalIds: string[]) => { - const points = { - R: 0, - E: 0, - } - const details = {}; - for (const portalId of portalIds) { - const portal = window.portals[portalId]; - if (portal && portal.options && portal.options.data) { - const owner = portal.options.data.team; - points[owner]++; - details[portal.options.guid] = { - ...portal.options.data, - guid: portal.options.guid, - } - } - } - return { points, details }; - } - -} diff --git a/src/colors.ts b/src/colors.ts new file mode 100644 index 0000000..146d4d9 --- /dev/null +++ b/src/colors.ts @@ -0,0 +1,144 @@ +// These are X11 colors stolen from https://en.wikipedia.org/wiki/Web_colors + +export default { + 'pink': [255, 192, 203], + 'lightpink': [255, 182, 193], + 'hotpink': [255, 105, 180], + 'deeppink': [255, 20, 147], + 'palevioletred': [219, 112, 147], + 'mediumvioletred': [199, 21, 133], + 'lightsalmon': [255, 160, 122], + 'salmon': [250, 128, 114], + 'darksalmon': [233, 150, 122], + 'lightcoral': [240, 128, 128], + 'indianred': [205, 92, 92], + 'crimson': [220, 20, 60], + 'firebrick': [178, 34, 34], + 'darkred': [139, 0, 0], + 'red': [255, 0, 0], + 'orangered': [255, 69, 0], + 'tomato': [255, 99, 71], + 'coral': [255, 127, 80], + 'darkorange': [255, 140, 0], + 'orange': [255, 165, 0], + 'yellow': [255, 255, 0], + 'lightyellow': [255, 255, 224], + 'lemonchiffon': [255, 250, 205], + 'lightgoldenrodyellow': [250, 250, 210], + 'papayawhip': [255, 239, 213], + 'moccasin': [255, 228, 181], + 'peachpuff': [255, 218, 185], + 'palegoldenrod': [238, 232, 170], + 'khaki': [240, 230, 140], + 'darkkhaki': [189, 183, 107], + 'gold': [255, 215, 0], + 'cornsilk': [255, 248, 220], + 'blanchedalmond': [255, 235, 205], + 'bisque': [255, 228, 196], + 'navajowhite': [255, 222, 173], + 'wheat': [245, 222, 179], + 'burlywood': [222, 184, 135], + 'tan': [210, 180, 140], + 'rosybrown': [188, 143, 143], + 'sandybrown': [244, 164, 96], + 'goldenrod': [218, 165, 32], + 'darkgoldenrod': [184, 134, 11], + 'Peru': [205, 133, 63], + 'chocolate': [210, 105, 30], + 'saddlebrown': [139, 69, 19], + 'sienna': [160, 82, 45], + 'brown': [165, 42, 42], + 'maroon': [128, 0, 0], + 'darkolivegreen': [85, 107, 47], + 'olive': [128, 128, 0], + 'olivedrab': [107, 142, 35], + 'yellowgreen': [154, 205, 50], + 'limegreen': [50, 205, 50], + 'lime': [0, 255, 0], + 'lawngreen': [124, 252, 0], + 'chartreuse': [127, 255, 0], + 'greenyellow': [173, 255, 47], + 'springgreen': [0, 255, 127], + 'mediumspringgreen': [0, 250, 154], + 'lightgreen': [144, 238, 144], + 'palegreen': [152, 251, 152], + 'darkseagreen': [143, 188, 143], + 'mediumaquamarine': [102, 205, 170], + 'mediumseagreen': [60, 179, 113], + 'seagreen': [46, 139, 87], + 'forestgreen': [34, 139, 34], + 'green': [0, 128, 0], + 'darkgreen': [0, 100, 0], + 'aqua': [0, 255, 255], + 'cyan': [0, 255, 255], + 'lightcyan': [224, 255, 255], + 'paleturquoise': [175, 238, 238], + 'aquamarine': [127, 255, 212], + 'turquoise': [64, 224, 208], + 'mediumturquoise': [72, 209, 204], + 'darkturquoise': [0, 206, 209], + 'lightseagreen': [32, 178, 170], + 'cadetblue': [95, 158, 160], + 'darkcyan': [0, 139, 139], + 'teal': [0, 128, 128], + 'lightsteelblue': [176, 196, 222], + 'powderblue': [176, 224, 230], + 'lightblue': [173, 216, 230], + 'skyblue': [135, 206, 235], + 'lightskyblue': [135, 206, 250], + 'deepskyblue': [0, 191, 255], + 'dodgerblue': [30, 144, 255], + 'cornflowerblue': [100, 149, 237], + 'steelblue': [70, 130, 180], + 'royalblue': [65, 105, 225], + 'blue': [0, 0, 255], + 'mediumblue': [0, 0, 205], + 'darkblue': [0, 0, 139], + 'navy': [0, 0, 128], + 'midnightblue': [25, 25, 112], + 'lavender': [230, 230, 250], + 'thistle': [216, 191, 216], + 'plum': [221, 160, 221], + 'violet': [238, 130, 238], + 'orchid': [218, 112, 214], + 'fuchsia': [255, 0, 255], + 'Magenta': [255, 0, 255], + 'mediumorchid': [186, 85, 211], + 'mediumpurple': [147, 112, 219], + 'blueviolet': [138, 43, 226], + 'darkviolet': [148, 0, 211], + 'darkorchid': [153, 50, 204], + 'darkmagenta': [139, 0, 139], + 'purple': [128, 0, 128], + 'indigo': [75, 0, 130], + 'darkslateblue': [72, 61, 139], + 'slateblue': [106, 90, 205], + 'mediumslateblue': [123, 104, 238], + 'white': [255, 255, 255], + 'snow': [255, 250, 250], + 'honeydew': [240, 255, 240], + 'mintcream': [245, 255, 250], + 'azure': [240, 255, 255], + 'aliceblue': [240, 248, 255], + 'ghostwhite': [248, 248, 255], + 'whitesmoke': [245, 245, 245], + 'seashell': [255, 245, 238], + 'beige': [245, 245, 220], + 'oldlace': [253, 245, 230], + 'floralwhite': [255, 250, 240], + 'ivory': [255, 255, 240], + 'antiquewhite': [250, 235, 215], + 'linen': [250, 240, 230], + 'lavenderblush': [255, 240, 245], + 'mistyrose': [255, 228, 225], + 'gainsboro': [220, 220, 220], + 'lightgray': [211, 211, 211], + 'silver': [192, 192, 192], + 'darkgray': [169, 169, 169], + 'gray': [128, 128, 128], + 'dimgray': [105, 105, 105], + 'lightslategray': [119, 136, 153], + 'slategray': [112, 128, 144], + 'darkslategray': [47, 79, 79], + 'black': [0, 0, 0], +} diff --git a/src/commonInterfaces.ts b/src/commonInterfaces.ts new file mode 100644 index 0000000..d9f30e4 --- /dev/null +++ b/src/commonInterfaces.ts @@ -0,0 +1,33 @@ +export interface BookmarksPortalInfo { + label: string; + latlng: string; + guid: string; +} + +export interface LatLng { + lat: number; + lng: number; +} + +export interface MininomalyEventSettings { + preparationTime?: number; + firstMeasurementTime: number; + measurementInterval: number; + numberOfMeasurements: number; +} + +export interface MeasurementPoints { + R: number; + E: number; + N?: number; +} + +export interface MininomalyGame { + prepareGame: (eventSettings: MininomalyEventSettings) => void; + prepareNextMeasurement: (nextMeasurement: number, nextMeasurementTime: number, eventSettings: MininomalyEventSettings) => void; + takeMeasurement: (currentMeasurement: number, currentMeasurementTime: number, eventSettings: MininomalyEventSettings) => void; + end: () => void; + + serialize: () => string; + deserialize: (serialized: string) => void; +} diff --git a/src/drawMap.ts b/src/drawMap.ts index 571c950..e580504 100644 --- a/src/drawMap.ts +++ b/src/drawMap.ts @@ -1,30 +1,78 @@ -import { BookmarksPortalInfo } from "./interfaces"; +import { LatLng } from "./commonInterfaces"; +import colors from "./colors"; -const mapLines = window.localStorage['PRIV.mapLines'] || []; +const mapLines = JSON.parse(window.localStorage['PRIV.mapLines']) || []; -const getLatLng = (latlng: string) => { +const getLatLngFromE6 = (latlng: string): LatLng => { const split = latlng.split(','); return { lat: +split[0], lng: +split[1] }; } -interface LatLng { - lat: number; - lng: number; -} +const degToRad = (deg: number) => deg * Math.PI / 180; interface Pos { x: number; y: number; } -export default (allPortals: BookmarksPortalInfo[], ornamentedPortals: BookmarksPortalInfo[], previousPortals: BookmarksPortalInfo[], measurementNumber: number, measurementTime: number) => { +export type Color = [number, number, number] | string; + +interface GenericMapSettings { + portals: LatLng[]; + drawPortals?: boolean; + measurementNumber?: number; + measurementTime?: number; + + ornamentedPortals?: Array<{ pos: LatLng; color?: Color; }>; + targetPortals?: Array<{ pos: LatLng; color?: Color; }>; + + lines?: Array<{ latLngs: LatLng[], color?: Color }>; + + legend?: Array<{ label: string; color: Color; }>; +} + +const DEFAULT_ORNAMENT_COLOR: Color = [0, 255, 236]; + +const getColor = (color: Color, alpha: number = 0) => { + let colorArray = color; + if (typeof color === 'string') { + colorArray = colors[color] || DEFAULT_ORNAMENT_COLOR; + } + const [r, g, b] = colorArray as [number, number, number]; + + return alpha > 0 ? `rgba(${r}, ${g}, ${b}, ${alpha})` : `rgb(${r}, ${g}, ${b})`; +} + +const drawNGon = (ctx: CanvasRenderingContext2D, centerX: number, centerY: number, r: number, n: number) => { + for (let i = 0; i <= n; ++i) { + const angle = degToRad(i * 360 / n); + const x = centerX + r * Math.sin(angle); + const y = centerY + r * Math.cos(angle); + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } +} + +export const drawMapGeneric = ({ + portals, + drawPortals, + measurementNumber, + measurementTime, + ornamentedPortals, + targetPortals, + lines, + legend, +}: GenericMapSettings) => { const canvas = document.createElement('canvas'); const posMin: LatLng = { lat: Infinity, lng: Infinity }; const posMax: LatLng = { lat: -Infinity, lng: -Infinity }; - for (const portal of allPortals) { - const pos = getLatLng(portal.latlng); + for (const portal of portals) { + const pos = portal; if (pos.lat < posMin.lat) posMin.lat = pos.lat; if (pos.lng < posMin.lng) posMin.lng = pos.lng; if (pos.lat > posMax.lat) posMax.lat = pos.lat; @@ -39,14 +87,16 @@ export default (allPortals: BookmarksPortalInfo[], ornamentedPortals: BookmarksP const lngLength = Math.cos(latMean * Math.PI / 180) * lngLengthAtEquator; const margins = 20; + const legendEntrySpace = 30; + const legendMargin = ((legend && legend.length || 0) + 1) * legendEntrySpace; const canvasWidth = Math.ceil(Math.abs((posMin.lng - posMax.lng) * multiplier * lngLength)) + 2 * margins; - const canvasHeight = Math.ceil(Math.abs((posMin.lat - posMax.lat) * multiplier * latLength)) + 2 * margins; + const canvasHeight = Math.ceil(Math.abs((posMin.lat - posMax.lat) * multiplier * latLength)) + 2 * margins + legendMargin; const latLngToXY = (latLng: LatLng): Pos => { return { x: (latLng.lng - posMin.lng) * multiplier * lngLength + margins, - y: canvasHeight - ((latLng.lat - posMin.lat) * multiplier * latLength + margins), + y: canvasHeight - ((latLng.lat - posMin.lat) * multiplier * latLength + margins) - legendMargin, } } @@ -73,68 +123,121 @@ export default (allPortals: BookmarksPortalInfo[], ornamentedPortals: BookmarksP ctx.lineTo(pos.x, pos.y); } ctx.stroke(); + ctx.closePath(); + } + + // draw lines + if (lines) { + for (const line of lines) { + const points = line.latLngs; + const first = points.shift(); + + ctx.strokeStyle = getColor(line.color || DEFAULT_ORNAMENT_COLOR); + ctx.lineWidth = 2; + ctx.beginPath(); + const pos = latLngToXY(first); + ctx.moveTo(pos.x, pos.y); + for (const point of points) { + const pos = latLngToXY(point); + ctx.lineTo(pos.x, pos.y); + } + ctx.stroke(); + ctx.closePath(); + } } - // draw ornaments - ctx.fillStyle = 'rgba(0, 255, 236, 0.3)'; - ctx.strokeStyle = 'rgb(0, 255, 236)'; + // draw ornaments (hexagonal) ctx.lineWidth = 2; - for (const portal of ornamentedPortals) { - const pos = latLngToXY(getLatLng(portal.latlng)); - const r = 10; - ctx.beginPath(); - ctx.moveTo(pos.x + r, pos.y); - ctx.lineTo(pos.x + r / 2, pos.y + r * Math.sqrt(3) / 2); - ctx.lineTo(pos.x - r / 2, pos.y + r * Math.sqrt(3) / 2); - ctx.lineTo(pos.x - r, pos.y); - ctx.lineTo(pos.x - r / 2, pos.y - r * Math.sqrt(3) / 2); - ctx.lineTo(pos.x + r / 2, pos.y - r * Math.sqrt(3) / 2); - ctx.lineTo(pos.x + r, pos.y); - ctx.stroke(); - ctx.fill(); - ctx.closePath(); + if (ornamentedPortals) { + for (const portal of ornamentedPortals) { + ctx.fillStyle = getColor(portal.color || DEFAULT_ORNAMENT_COLOR, 0.3); + ctx.strokeStyle = getColor(portal.color || DEFAULT_ORNAMENT_COLOR); + + const pos = latLngToXY(portal.pos); + const r = 10; + ctx.beginPath(); + drawNGon(ctx, pos.x, pos.y, r, 6); + ctx.stroke(); + ctx.fill(); + ctx.closePath(); + } + } + + // draw targets (triangles) + ctx.lineWidth = 2; + if (targetPortals) { + for (const portal of targetPortals) { + ctx.fillStyle = getColor(portal.color || DEFAULT_ORNAMENT_COLOR, 0.3); + ctx.strokeStyle = getColor(portal.color || DEFAULT_ORNAMENT_COLOR); + + const pos = latLngToXY(portal.pos); + const r = 18; + ctx.beginPath(); + drawNGon(ctx, pos.x, pos.y, r, 3); + ctx.stroke(); + ctx.fill(); + ctx.closePath(); + } } //PREV - if (previousPortals) { + if (drawPortals) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 2; - for (const portal of previousPortals) { - const pos = latLngToXY(getLatLng(portal.latlng)); - const r = 5; - // ctx.fillRect((pos.lng - posMin.lng) * multiplier - 5, canvasHeight - (pos.lat - posMin.lat) * multiplier - 5, 10, 10); - ctx.beginPath() - ctx.arc( - pos.x, - pos.y, - r, - 0, - Math.PI * 2, - ); - ctx.stroke(); - ctx.closePath(); - // ctx.fillRect(, 10, 10); + if (portals) { + for (const portal of portals) { + const pos = latLngToXY(portal); + const r = 5; + // ctx.fillRect((pos.lng - posMin.lng) * multiplier - 5, canvasHeight - (pos.lat - posMin.lat) * multiplier - 5, 10, 10); + ctx.beginPath() + ctx.arc( + pos.x, + pos.y, + r, + 0, + Math.PI * 2, + ); + ctx.stroke(); + ctx.closePath(); + // ctx.fillRect(, 10, 10); + } } } + // draw bg + ctx.fillStyle = 'rgb(20, 20, 20)'; + ctx.fillRect(0, canvasHeight - legendMargin, canvasWidth, canvasHeight); + + // draw legend const measurementDate = new Date(measurementTime); const timeText = `${measurementDate.getHours()}:${(measurementDate.getMinutes() < 10 ? '0' : '') + measurementDate.getMinutes()}`; ctx.fillStyle = 'rgb(0, 255, 236)'; - ctx.font = '14px Coda'; + ctx.font = '14px Coda, Roboto, Arial'; ctx.textAlign = 'right'; ctx.fillText(`measurement at ${timeText}`, canvasWidth - 5, canvasHeight - 5); - // - // const imageData = canvas.toDataURL("image/png"); - - // const image = document.createElement('img'); - // image.setAttribute('style', 'position:absolute;top:0;left:0;z-index:9999999999999;'); - // image.setAttribute('src', imageData); - // document.body.appendChild(image); - // canvas.toBlob((blob) => { - // TgBotCommunicator.sendImage(blob); - // }); + if (legend) { + legend.forEach((entry, i) => { + ctx.fillStyle = 'rgb(180, 180, 180)'; + ctx.textAlign = 'left'; + ctx.fillText(entry.label, 40, canvasHeight - legendMargin + i * legendEntrySpace + 1.5 * legendEntrySpace - 10); + + ctx.fillStyle = getColor(entry.color || DEFAULT_ORNAMENT_COLOR, 0.3); + ctx.strokeStyle = getColor(entry.color || DEFAULT_ORNAMENT_COLOR); + + const pos = { + x: 20, + y: canvasHeight - legendMargin + i * legendEntrySpace + legendEntrySpace + }; + const r = 10; + ctx.beginPath(); + drawNGon(ctx, pos.x, pos.y, r, 6); + ctx.stroke(); + ctx.fill(); + ctx.closePath(); + }); + } return canvas; } diff --git a/src/iitcHelpers.ts b/src/iitcHelpers.ts new file mode 100644 index 0000000..6f1f497 --- /dev/null +++ b/src/iitcHelpers.ts @@ -0,0 +1,18 @@ +import { LatLng, BookmarksPortalInfo } from "./commonInterfaces"; + +export const getPortalLocation = (portalId: string): LatLng => { + const portal = window.portals[portalId]; + return portal && portal._latlng; +} + +export const getPortalData = (portalId: string) => { + const portal = window.portals[portalId]; + return portal && portal.options && portal.options.data; +} + +export const getLatLngFromE6String = (latlng: string): LatLng => { + const split = latlng.split(','); + return { lat: +split[0], lng: +split[1] }; +} + +export const getLatLngFromBookmark = (portal: BookmarksPortalInfo) => getLatLngFromE6String(portal.latlng); diff --git a/src/index.ts b/src/index.ts index c4a4d0a..f76f587 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,51 @@ -import TgBotCommunicator from "./TgBotCommunicator"; -import MiniNomalyPlugin from "./MininomalyPlugin"; -import NoopCommunicator from "./NoopCommunicator"; -import { BotCommunicator } from "./interfaces"; +import TgBotCommunicator from "./portalBattle/TgBotCommunicator"; +import NoopCommunicator from "./portalBattle/NoopCommunicator"; +import { BotCommunicator } from "./portalBattle/interfaces"; +import PortalBattleGame from "./portalBattle/PortalBattleGame"; +import Mininomaly from "./Mininomaly"; +import TeamShardsGame, { ShardsBotCommunicator } from "./teamShards/TeamShardsGame"; +import ShardsNoopCommunicator from "./teamShards/ShardsNoopCommunicator"; +import ShardsTgBotCommunicator from "./teamShards/ShardsTgBotCommunicator"; + +if (typeof window.plugin === 'undefined') { + window.plugin = {}; +} if (window.localStorage['PRIV.initMininomalyAutomatically'] === 'true') { - let botCommunicator: BotCommunicator; - if (window.localStorage['PRIV.tgBotToken'] && window.localStorage['PRIV.tgChatId']) { - botCommunicator = new TgBotCommunicator(window.localStorage['PRIV.tgBotToken'], +window.localStorage['PRIV.tgChatId'], /*'šŸš§ MINI-ANOMALY TEST šŸš§\n'*/'', false); + if (window.localStorage['PRIV.game'] === 'portalBattle') { + let botCommunicator: BotCommunicator; + + if (window.localStorage['PRIV.tgBotToken'] && window.localStorage['PRIV.tgChatId']) { + botCommunicator = new TgBotCommunicator(window.localStorage['PRIV.tgBotToken'], +window.localStorage['PRIV.tgChatId'], /*'šŸš§ MINI-ANOMALY TEST šŸš§\n'*/'', true); + } else { + botCommunicator = new NoopCommunicator(); + } + + const portalBattle = new PortalBattleGame({ botCommunicator }); + const engine = new Mininomaly({ game: portalBattle }); + + window.plugin.mininomaly = { + portalBattle, + engine + }; + } else if (window.localStorage['PRIV.game'] === 'teamShards') { + let botCommunicator: ShardsBotCommunicator; + + if (window.localStorage['PRIV.tgBotToken'] && window.localStorage['PRIV.tgChatId']) { + botCommunicator = new ShardsTgBotCommunicator(window.localStorage['PRIV.tgBotToken'], +window.localStorage['PRIV.tgChatId'], /*'šŸš§ MINI-ANOMALY TEST šŸš§\n'*/''); + } else { + botCommunicator = new ShardsNoopCommunicator(); + } + + const teamShards = new TeamShardsGame({ botCommunicator }); + const engine = new Mininomaly({ game: teamShards }); + + window.plugin.mininomaly = { + teamShards, + engine + }; } else { - botCommunicator = new NoopCommunicator(); + console.warn('Mininomaly game not chosen!'); } - const miniNomalyPlugin = new MiniNomalyPlugin({ botCommunicator }); - window.plugin.miniNomalyPlugin = miniNomalyPlugin; + } diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index 1678fca..0000000 --- a/src/interfaces.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface BookmarksPortalInfo { - label: string; - latlng: string; - guid: string; -} - -export interface MiniNomalySettings { - firstMeasurementTime: number; - measurementInterval: number; - numberOfMeasurements: number; - portalsNumber: number; - bookmarksFolders: string[]; - bonuses: MeasurementPoints; -} - -export interface MeasurementPoints { - R: number; - E: number; - N?: number; -} - -export interface MeasurementData { - portals: string[]; - points: MeasurementPoints; - details: any; -} - -export interface BotCommunicator { - sendNextMeasurementList(measurementNumber: number, time: number, portals: BookmarksPortalInfo[]): void; - sendNextMeasurementImage(allPortals: BookmarksPortalInfo[], ornamented: BookmarksPortalInfo[], previous: BookmarksPortalInfo[], measurementNumber: number, measurementTime: number): void; - sendMeasurementResult(measurementNumber: number, numberOfMeasurements: number, points: MeasurementPoints, totalPoints: MeasurementPoints): void; -} diff --git a/src/NoopCommunicator.ts b/src/portalBattle/NoopCommunicator.ts similarity index 87% rename from src/NoopCommunicator.ts rename to src/portalBattle/NoopCommunicator.ts index 93da40f..8bf2afe 100644 --- a/src/NoopCommunicator.ts +++ b/src/portalBattle/NoopCommunicator.ts @@ -1,4 +1,5 @@ -import { BookmarksPortalInfo, MeasurementPoints, BotCommunicator } from "./interfaces"; +import { BookmarksPortalInfo, MeasurementPoints } from "../commonInterfaces"; +import { BotCommunicator } from "./interfaces"; export default class NoopCommunicator implements BotCommunicator { diff --git a/src/portalBattle/PortalBattleGame.ts b/src/portalBattle/PortalBattleGame.ts new file mode 100644 index 0000000..136e446 --- /dev/null +++ b/src/portalBattle/PortalBattleGame.ts @@ -0,0 +1,157 @@ +import { MininomalyGame, MininomalyEventSettings, BookmarksPortalInfo, MeasurementPoints } from "../commonInterfaces"; +import { BotCommunicator } from "./interfaces"; + +interface PortalBattleSettings { + portalsNumber: number; + bookmarksFolders?: string[]; + bonuses?: MeasurementPoints; +} + +interface PortalBattleMeasurementData { + portals: string[]; + points: MeasurementPoints; + details: any; +} + +export default class PortalBattleGame implements MininomalyGame { + measurements: PortalBattleMeasurementData[] = []; + settings: PortalBattleSettings; + botCommunicator: BotCommunicator; + + constructor({ botCommunicator }: { botCommunicator: BotCommunicator; }) { + this.botCommunicator = botCommunicator; + } + + public initSettings = (settings: PortalBattleSettings) => { + this.settings = { + portalsNumber: settings.portalsNumber, + bookmarksFolders: settings.bookmarksFolders ? [...settings.bookmarksFolders] : ['*'], + bonuses: settings.bonuses ? { ...settings.bonuses } : { E: 1, R: 1 }, + }; + } + + public prepareGame = (eventSettings: MininomalyEventSettings) => { + // nothing to prepare + }; + + public prepareNextMeasurement = (nextMeasurement: number, nextMeasurementTime: number, eventSettings: MininomalyEventSettings) => { + const nextMeasurementPortals = this.chooseRandomPortals(this.settings.portalsNumber); + this.measurements.push({ + portals: nextMeasurementPortals.map((p) => p.guid), + points: null, + details: null, + }); + + // bot communication about next measurement + this.botCommunicator.sendNextMeasurementList( + nextMeasurement, + nextMeasurementTime, + nextMeasurementPortals + ); + this.botCommunicator.sendNextMeasurementImage( + this.getAllPlayboxPortals(), + nextMeasurementPortals, + this.getAllPlayboxPortals(), + nextMeasurement, + nextMeasurementTime, + ); + }; + + public takeMeasurement = (currentMeasurement: number, currentMeasurementTime: number, eventSettings: MininomalyEventSettings) => { + const measurementResult = this.getRawScore(this.measurements[currentMeasurement].portals); + console.log(measurementResult); + this.measurements[currentMeasurement].points = measurementResult.points; + this.measurements[currentMeasurement].details = measurementResult.details; + + // bot communication about current measurement results + const total = this.calculateTotalResult(); + this.botCommunicator.sendMeasurementResult(currentMeasurement, eventSettings.numberOfMeasurements, measurementResult.points, total); + }; + + public end = () => { + console.log('the end'); + }; + + public serialize = () => JSON.stringify({ + measurements: this.measurements, + settings: this.settings, + }); + + public deserialize = (serialized: string) => { + const deserialized = JSON.parse(serialized); + this.measurements = deserialized.measurements as PortalBattleMeasurementData[]; + this.settings = deserialized.settings as PortalBattleSettings; + }; + + private chooseRandomPortals = (howMany: number): BookmarksPortalInfo[] => { + const all = this.getAllPlayboxPortals(); + + const chosen = []; + for (let i = 0; i < howMany; ++i) { + let index = Math.floor(Math.random() * all.length); + if (index === all.length) { + index = all.length - 1; + } + + chosen.push(all.splice(index, 1)[0]); + } + + return chosen; + } + + private getAllPlayboxPortals = () => { + const bkm = window.plugin && window.plugin.bookmarks && window.plugin.bookmarks.bkmrksObj && window.plugin.bookmarks.bkmrksObj.portals; + + if (!bkm) return; + + const all = []; + for (const folderId in bkm) { + const folder = bkm[folderId]; + const folderName = folder.label; + if (this.settings.bookmarksFolders.indexOf(folderName) >= 0 || this.settings.bookmarksFolders.indexOf(folderId) >= 0 || this.settings.bookmarksFolders.indexOf('*') >= 0) { + for (const portalId in folder.bkmrk) { + const portal = folder.bkmrk[portalId]; + + all.push(portal); + } + } + } + + return all; + } + + private getRawScore = (portalIds: string[]) => { + const points = { + R: 0, + E: 0, + } + const details = {}; + for (const portalId of portalIds) { + const portal = window.portals[portalId]; + if (portal && portal.options && portal.options.data) { + const owner = portal.options.data.team; + points[owner]++; + details[portal.options.guid] = { + ...portal.options.data, + guid: portal.options.guid, + } + } + } + return { points, details }; + } + + private calculateTotalResult = () => { + const total = { E: 0, R: 0 }; + for (const m of this.measurements) { + if (m.points) { + if (m.points.E * this.settings.bonuses.E > total.E) { + total.E = m.points.E * this.settings.bonuses.E; + } + if (m.points.R * this.settings.bonuses.R > total.R) { + total.R = m.points.R * this.settings.bonuses.R; + } + } + } + return total; + } +} diff --git a/src/TgBotCommunicator.ts b/src/portalBattle/TgBotCommunicator.ts similarity index 86% rename from src/TgBotCommunicator.ts rename to src/portalBattle/TgBotCommunicator.ts index e524c27..26f5b58 100644 --- a/src/TgBotCommunicator.ts +++ b/src/portalBattle/TgBotCommunicator.ts @@ -1,5 +1,7 @@ -import { BookmarksPortalInfo, MeasurementPoints, BotCommunicator } from "./interfaces"; -import drawMap from "./drawMap"; +import { BookmarksPortalInfo, MeasurementPoints } from "../commonInterfaces"; +import { BotCommunicator } from "./interfaces"; +import { drawMapGeneric } from "../drawMap"; +import { getLatLngFromBookmark } from "../iitcHelpers"; export default class TgBotCommunicator implements BotCommunicator { botToken: string; @@ -37,7 +39,13 @@ export default class TgBotCommunicator implements BotCommunicator { } sendNextMeasurementImage = (allPortals: BookmarksPortalInfo[], ornamented: BookmarksPortalInfo[], previous: BookmarksPortalInfo[], measurementNumber: number, measurementTime: number) => { - const canvas = drawMap(allPortals, ornamented, previous, measurementNumber, measurementTime); + const canvas = drawMapGeneric({ + drawPortals: true, + portals: allPortals.map(getLatLngFromBookmark), + ornamentedPortals: ornamented.map((portal) => ({ pos: getLatLngFromBookmark(portal) })), + measurementNumber, + measurementTime, + }); canvas.toBlob((blob) => { this.sendImage(blob); diff --git a/src/portalBattle/interfaces.ts b/src/portalBattle/interfaces.ts new file mode 100644 index 0000000..9b03a07 --- /dev/null +++ b/src/portalBattle/interfaces.ts @@ -0,0 +1,7 @@ +import { BookmarksPortalInfo, MeasurementPoints } from "../commonInterfaces"; + +export interface BotCommunicator { + sendNextMeasurementList(measurementNumber: number, time: number, portals: BookmarksPortalInfo[]): void; + sendNextMeasurementImage(allPortals: BookmarksPortalInfo[], ornamented: BookmarksPortalInfo[], previous: BookmarksPortalInfo[], measurementNumber: number, measurementTime: number): void; + sendMeasurementResult(measurementNumber: number, numberOfMeasurements: number, points: MeasurementPoints, totalPoints: MeasurementPoints): void; +} diff --git a/src/teamShards/ShardsNoopCommunicator.ts b/src/teamShards/ShardsNoopCommunicator.ts new file mode 100644 index 0000000..8618f16 --- /dev/null +++ b/src/teamShards/ShardsNoopCommunicator.ts @@ -0,0 +1,17 @@ +import { ShardsBotCommunicator, TeamInfo } from "./TeamShardsGame"; +import { MininomalyEventSettings, LatLng } from "../commonInterfaces"; + +export default class ShardsNoopCommunicator implements ShardsBotCommunicator { + sendInfoImage(currentJump: number, eventSettings: MininomalyEventSettings, portals: LatLng[], teams: TeamInfo[]): void { + console.log('sendStartEndPoints', currentJump, eventSettings, portals, teams); + } + + sendStartEndPoints(eventSettings: MininomalyEventSettings, teams: TeamInfo[]): void { + console.log('sendStartEndPoints', eventSettings, teams); + } + + sendJumpResult(currentMeasurement: number, eventSettings: MininomalyEventSettings, teams: TeamInfo[], currentJumpPoints: number[], currentJumpPartialPoints: number[]): void { + console.log('sendJumpResult', currentMeasurement, eventSettings, teams, currentJumpPoints, currentJumpPartialPoints); + } + +} diff --git a/src/teamShards/ShardsTgBotCommunicator.ts b/src/teamShards/ShardsTgBotCommunicator.ts new file mode 100644 index 0000000..7a90852 --- /dev/null +++ b/src/teamShards/ShardsTgBotCommunicator.ts @@ -0,0 +1,161 @@ +import { ShardsBotCommunicator, TeamInfo } from "./TeamShardsGame"; +import { MininomalyEventSettings, LatLng } from "../commonInterfaces"; +import { getPortalData } from "../iitcHelpers"; +import { drawMapGeneric, Color } from "../drawMap"; + +const portalDataToLatLng = (data: PortalData): LatLng => ({ + lat: data.latE6 / 1e6, + lng: data.lngE6 / 1e6, +}); + +export default class ShardsTgBotCommunicator implements ShardsBotCommunicator { + botToken: string; + chatId: number; + messagesPrefix: string; + + constructor(botToken: string, chatId: number, messagesPrefix: string = '') { + this.botToken = botToken; + this.chatId = chatId; + this.messagesPrefix = messagesPrefix; + } + + sendStartEndPoints = async (eventSettings: MininomalyEventSettings, teams: TeamInfo[]) => { + for (let i = 0; i < teams.length; ++i) { + const team = teams[i]; + await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, { + method: 'POST', + body: JSON.stringify({ + chat_id: this.chatId, + parse_mode: 'Markdown', + disable_web_page_preview: true, + text: `${this.messagesPrefix}Start location for *${team.name}*:`, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + await fetch(`https://api.telegram.org/bot${this.botToken}/sendLocation`, { + method: 'POST', + body: JSON.stringify({ + chat_id: this.chatId, + latitude: team.from.lat, + longitude: team.from.lng, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + } + + sendJumpResult(currentMeasurement: number, eventSettings: MininomalyEventSettings, teams: TeamInfo[], currentJumpPoints: number[], currentJumpPartialPoints: number[]): void { + + console.log('sendJumpResult', currentMeasurement, eventSettings, teams, currentJumpPoints, currentJumpPartialPoints); + + const isLastJump = currentMeasurement === eventSettings.numberOfMeasurements - 1; + const beforeFirstJump = currentMeasurement === -1; + + const teamsInfo = teams.map((team, i) => { + const targetPortal = getPortalData(team.currentTarget); + const latlng = `${targetPortal.latE6 / 1e6},${targetPortal.lngE6 / 1e6}`; + const targetInfo = `Target #${i + 1}: ${targetPortal.title} - [Intel](https://intel.ingress.com/intel?ll=${latlng}&z=17&pll=${latlng}) - [GMaps](https://maps.google.com/maps?ll=${latlng}&q=${latlng}%20%28${escape(targetPortal.title)}%29)`; + + const header = `Team *${team.name}*: ${team.points} pts`; + + const pointsScored = currentJumpPoints[i] >= 1 ? `\nā­ Scored a point now!` : ''; + + const shardsList = team.shards.map((shard) => { + const p = getPortalData(shard.portal); + const latlng = `${p.latE6 / 1e6},${p.lngE6 / 1e6}`; // 50.064085,19.934536 + return `Shard #${i + 1}: ${p.title} - [Intel](https://intel.ingress.com/intel?ll=${latlng}&z=17&pll=${latlng}) - [GMaps](https://maps.google.com/maps?ll=${latlng}&q=${latlng}%20%28${escape(p.title)}%29)`; + }).join('\n'); + + return header + pointsScored + '\n' + targetInfo + '\n' + shardsList; + }).join('\n\n'); + + const d = new Date(eventSettings.firstMeasurementTime + (currentMeasurement + (beforeFirstJump ? 1 : 0)) * eventSettings.measurementInterval); + const timeText = `${d.getHours()}:${(d.getMinutes() < 10 ? '0' : '') + d.getMinutes()}`; + + const teamsTempResults = beforeFirstJump ? '' : teams + .map((team, i) => ({ + name: team.name, + i, + points: team.points + (isLastJump ? 0 : currentJumpPartialPoints[i]), + })) + .sort((a, b) => b.points - a.points) + .map((team, n) => `${n + 1}. ${team.name} (${team.points.toFixed(2)})`) + .join('\n'); + + const header = beforeFirstJump + ? `*Positions of shards and targets for the first jump (at ${timeText}):*\n\n` + : `*Results of jump ${currentMeasurement + 1} (at ${timeText}):*\n\n`; + + fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, { + method: 'POST', + body: JSON.stringify({ + chat_id: this.chatId, + parse_mode: 'Markdown', + disable_web_page_preview: true, + text: this.messagesPrefix + + header + + teamsInfo + + (beforeFirstJump ? '' : `\n\n----------Ranking-----------\n`) + + teamsTempResults, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + private sendImage = (imageBlob) => { + const formData = new FormData(); + + formData.append('photo', imageBlob); + + fetch(`https://api.telegram.org/bot${this.botToken}/sendPhoto?chat_id=${this.chatId}`, { + method: 'POST', + body: formData + }); + } + + private teamsColors: Color[] = ['red', 'green', 'orange', 'blue', 'cyan', 'purple', 'turquoise', 'grey', 'dodgerblue']; + public sendInfoImage = (currentJump: number, eventSettings: MininomalyEventSettings, portals: LatLng[], teams: TeamInfo[]) => { + const canvas = drawMapGeneric({ + portals, + drawPortals: true, + measurementTime: eventSettings.firstMeasurementTime + currentJump * eventSettings.measurementInterval, + ornamentedPortals: teams + .filter((team) => team.shards && team.shards.length) + .map((team, i) => ({ + pos: portalDataToLatLng(getPortalData(team.shards[0].portal)), + color: this.teamsColors[i] + })), + + targetPortals: teams + .filter((team) => team.currentTarget) + .map((team, i) => ({ + pos: portalDataToLatLng(getPortalData(team.currentTarget)), + color: this.teamsColors[i] + })), + + lines: teams + .filter((team) => team.shards && team.shards.length) + .map((team, i) => ({ + latLngs: team.shards[0].history.slice().reverse().slice(0, 2).map((portal) => portalDataToLatLng(getPortalData(portal))), + color: this.teamsColors[i] + })), + + legend: teams + .map((team, i) => ({ + label: `[#${i + 1}] ${team.name}`, + color: this.teamsColors[i] + })), + }); + + canvas.toBlob((blob) => { + this.sendImage(blob); + }); + } + +} diff --git a/src/teamShards/TeamShardsGame.ts b/src/teamShards/TeamShardsGame.ts new file mode 100644 index 0000000..526cae1 --- /dev/null +++ b/src/teamShards/TeamShardsGame.ts @@ -0,0 +1,348 @@ +import { MininomalyGame, MininomalyEventSettings, LatLng } from "../commonInterfaces"; +import visualizeOnIITC from "./visualizeOnIITC"; +import { getPortalLocation } from "../iitcHelpers"; + +export interface ShardInfo { + history: string[]; // portals ids list + portal: string; // portal id +} + +export interface TeamInfo { + name: string; + from: LatLng; + to: LatLng; + currentTarget: string; // portal id + shards: ShardInfo[]; // portals ids + points: number; +} + +export interface ShardsBotCommunicator { + sendStartEndPoints(eventSettings: MininomalyEventSettings, teams: TeamInfo[]): void; + sendJumpResult(currentJump: number, eventSettings: MininomalyEventSettings, teams: TeamInfo[], currentJumpPoints: number[], currentJumpPartialPoints: number[]): void; + sendInfoImage(currentJump: number, eventSettings: MininomalyEventSettings, portals: LatLng[], teams: TeamInfo[]): void; +} + +const degToRad = (deg: number) => deg * Math.PI / 180; +const latLength = 110.57; // in km +const lngLengthAtEquator = 111.32; // in km + +const randomIndex = (arrLength: number) => Math.floor(Math.random() * arrLength); + +export default class TeamShardsGame implements MininomalyGame { + botCommunicator: ShardsBotCommunicator; + + // serializable settings + targetsMiddlePoint: LatLng; + targetsRadius: number; // in meters + playzoneRadius: number; // in meters + teams: TeamInfo[]; + blacklistedPortals: string[]; + waitBeforeRandomJump: number; + + // TODO: make configurable in future + readonly howManyShards: number = 1; + + constructor({ botCommunicator }) { + this.botCommunicator = botCommunicator; + } + + public initSettings = (targetsMiddlePoint: LatLng, targetsRadius: number, playzoneRadius: number, teams: string[], waitBeforeRandomJump: number = 0, blacklistedPortals: string[] = []) => { + this.targetsMiddlePoint = { ...targetsMiddlePoint }; + this.targetsRadius = targetsRadius; + this.playzoneRadius = playzoneRadius; + this.teams = teams.map((teamName) => ({ + name: teamName, + from: null, + to: null, + currentTarget: '', + shards: [], + points: 0 + })); + this.blacklistedPortals = blacklistedPortals; + this.waitBeforeRandomJump = waitBeforeRandomJump; + } + + public prepareGame = (eventSettings: MininomalyEventSettings) => { + // randomly draw start points + const numOfTeams = this.teams.length; + const degreesStep = 180 / numOfTeams; + const randomOffset = Math.random() * 360; + const centerLat = this.targetsMiddlePoint.lat; + const centerLng = this.targetsMiddlePoint.lng; + + for (let i = 0; i < numOfTeams; ++i) { + const angle = i * degreesStep + (i % 2) * 180 + randomOffset; + const r = this.targetsRadius; // in meters + + const spawnLat = centerLat + r * Math.sin(degToRad(angle)) / (latLength * 1000); + const spawnLngLength = Math.cos(spawnLat * Math.PI / 180) * lngLengthAtEquator; // in km + const spawnLng = centerLng + r * Math.cos(degToRad(angle)) / (spawnLngLength * 1000); + + const targetLat = centerLat + r * Math.sin(degToRad(angle + 180)) / (latLength * 1000); + const targetLngLength = Math.cos(targetLat * Math.PI / 180) * lngLengthAtEquator; // in km + const targetLng = centerLng + r * Math.cos(degToRad(angle + 180)) / (targetLngLength * 1000); + + this.teams[i].from = { lat: spawnLat, lng: spawnLng }; + this.teams[i].to = { lat: targetLat, lng: targetLng }; + } + + this.botCommunicator.sendStartEndPoints(eventSettings, this.teams); + + // visualize on iitc + const DEBUG_visualizeOnDrawTools = !!+localStorage['DEBUG_visualizeOnDrawTools']; + if (DEBUG_visualizeOnDrawTools) { + visualizeOnIITC({ + targetsMiddlePoint: this.targetsMiddlePoint, + targetsRadius: this.targetsRadius, + playzoneRadius: this.playzoneRadius, + teams: this.teams, + getPortalLocation: this.getPortalLocation, + }); + } + }; + + public prepareNextMeasurement = (nextMeasurement: number, nextMeasurementTime: number, eventSettings: MininomalyEventSettings) => { + this.teams.forEach((team) => { + if (!team.shards.length) { // this will happen only on the preparation of the first measurement + this.chooseTargetsAndShards(team); + } + }); + + if (nextMeasurement === 0) { + this.botCommunicator.sendJumpResult(-1, eventSettings, this.teams, this.teams.map(() => 0), this.teams.map(() => 0)); + this.botCommunicator.sendInfoImage(-1, eventSettings, this.getAllPlayzonePortals().map((portal) => getPortalLocation(portal)), this.teams); + } + + // visualize on iitc + const DEBUG_visualizeOnDrawTools = !!+localStorage['DEBUG_visualizeOnDrawTools']; + if (nextMeasurement === 0 && DEBUG_visualizeOnDrawTools) { + visualizeOnIITC({ + targetsMiddlePoint: this.targetsMiddlePoint, + targetsRadius: this.targetsRadius, + playzoneRadius: this.playzoneRadius, + teams: this.teams, + getPortalLocation: this.getPortalLocation, + }); + } + }; + + public takeMeasurement = (currentMeasurement: number, currentMeasurementTime: number, eventSettings: MininomalyEventSettings) => { + const currentJumpPoints = []; + const currentJumpPartialPoints = []; + this.teams.forEach((team, i) => { + // perform shards jumps + team.shards = team.shards.map((shard) => { + const destinations: string[] = this.getPossibleShardDestinations(shard); + + const destination = destinations[randomIndex(destinations.length)]; + return { + history: [...shard.history, destination], + portal: destination + }; + }); + + // if shard is in target, delete it and score a point + let points = team.shards.length; + team.shards = team.shards.filter((shard) => shard.portal !== team.currentTarget); + points -= team.shards.length; + team.points += points; + currentJumpPoints[i] = points; + + // calculate the partial results + currentJumpPartialPoints[i] = 0; + team.shards.forEach((shard) => { + const pos = this.getPortalLocation(shard.portal); + const originLocation = this.getPortalLocation(shard.history[0]); + const targetLocation = this.getPortalLocation(team.currentTarget); + const shardToTarget = this.dist(pos, targetLocation); + const originToTarget = this.dist(originLocation, targetLocation); + + const partialPoint = 1 - shardToTarget / originToTarget; + currentJumpPartialPoints[i] += partialPoint; + }); + + // the last measurement + if (currentMeasurement === eventSettings.numberOfMeasurements - 1) { + // add partial points to teams points + team.points += currentJumpPartialPoints[i]; + } + + // if the team has no shards + if (!team.shards.length) { + // swap spawn and target points + const newTo = team.from; + team.from = team.to; + team.to = newTo; + + this.chooseTargetsAndShards(team); + } + }); + + // bot communication + this.botCommunicator.sendJumpResult(currentMeasurement, eventSettings, this.teams, currentJumpPoints, currentJumpPartialPoints); + this.botCommunicator.sendInfoImage(currentMeasurement, eventSettings, this.getAllPlayzonePortals().map((portal) => getPortalLocation(portal)), this.teams); + + // visualize on iitc + const DEBUG_visualizeOnDrawTools = !!+localStorage['DEBUG_visualizeOnDrawTools']; + if (DEBUG_visualizeOnDrawTools) { + visualizeOnIITC({ + targetsMiddlePoint: this.targetsMiddlePoint, + targetsRadius: this.targetsRadius, + playzoneRadius: this.playzoneRadius, + teams: this.teams, + getPortalLocation: this.getPortalLocation, + }); + } + }; + + private getAllPlayzonePortals = (): string[] => + Object.keys(window.portals).filter((portalId) => { + const pos = this.getPortalLocation(portalId); + return this.isInPlayzone(pos); + }); + + + private chooseTargetsAndShards = (team: TeamInfo) => { + // choose new target + const possibleTargets = this.chooseClosestPortals(team.to, 3); + team.currentTarget = possibleTargets[randomIndex(possibleTargets.length)]; + + // choose new shard + // TODO: chosing more than one + const possibleShards = this.chooseClosestPortals(team.from, 3); + const portal = possibleShards[randomIndex(possibleShards.length)]; + team.shards.push({ + history: [portal], + portal, + }); + }; + + private getPossibleShardDestinations = (shard: ShardInfo): string[] => { + let destinations: string[] = []; + if (window.links) { + Object.values(window.links).forEach((link) => { + if (link.options && link.options.data) { + let dest: string; + let destLatLng: LatLng; + + // check if any end of link is at given portal + if (link.options.data.dGuid === shard.portal) { + dest = link.options.data.oGuid; + destLatLng = { + lat: link.options.data.oLatE6 / 1e6, + lng: link.options.data.oLngE6 / 1e6, + }; + } else if (link.options.data.oGuid === shard.portal) { + dest = link.options.data.dGuid; + destLatLng = { + lat: link.options.data.dLatE6 / 1e6, + lng: link.options.data.dLngE6 / 1e6, + }; + } + + // check if found possible destination is not outside configured zone + // and if it's not in a shard history + // TODO: make configurable time of shard backtracking prevention (use only a part of history) + if (dest && this.dist(this.targetsMiddlePoint, destLatLng) <= this.playzoneRadius && shard.history.indexOf(dest) === -1) { + destinations.push(dest); + } + } + }); + } + + // check if destinations is not empty + if (!destinations.length) { + // if it is, check if random jump should occur or if shard should wait + let count = 0; + for (let i = shard.history.length - 1; i >= 0; --i) { + if (shard.history[i] !== shard.portal) { + break; + } + ++count; + } + + if (count > this.waitBeforeRandomJump) { + // if the limit of skipped jumps is exceeded, choose some closest portals + // except from the portals it was previously on + return this.chooseClosestPortals(this.getPortalLocation(shard.portal), 3, shard.history); + } else { + // if it is not, wait on the same portal + return [shard.portal]; + } + } + + return destinations; + } + + public end = () => { + + }; + + public serialize = () => JSON.stringify({ + targetsMiddlePoint: this.targetsMiddlePoint, + targetsRadius: this.targetsRadius, + playzoneRadius: this.playzoneRadius, + teams: this.teams, + blacklistedPortals: this.blacklistedPortals, + waitBeforeRandomJump: this.waitBeforeRandomJump, + }); + + public deserialize = (serialized: string) => { + const deserialized = JSON.parse(serialized); + + this.targetsMiddlePoint = deserialized.targetsMiddlePoint; + this.targetsRadius = deserialized.targetsRadius; + this.playzoneRadius = deserialized.playzoneRadius; + this.teams = deserialized.teams; + this.blacklistedPortals = deserialized.blacklistedPortals; + this.waitBeforeRandomJump = deserialized.waitBeforeRandomJump; + }; + + private dist = (a: LatLng, b: LatLng) => { + var p = Math.PI / 180; + var c = Math.cos; + var aa = 0.5 - c((b.lat - a.lat) * p) / 2 + + c(a.lat * p) * c(b.lat * p) * + (1 - c((b.lng - a.lng) * p)) / 2; + + return 12742000 * Math.asin(Math.sqrt(aa)); // 2 * R; R = 6371 km + } + + private isInPlayzoneCache = {}; + private isInPlayzone = (pos: LatLng): boolean => { + const key = pos.lat + ',' + pos.lng; + const cached = this.isInPlayzoneCache[key]; + if (typeof cached == 'boolean') { + return cached; + } + const dist = this.dist(this.targetsMiddlePoint, pos); + const isIn = dist <= this.playzoneRadius; + this.isInPlayzoneCache[key] = isIn; + return isIn; + } + + private chooseClosestPortals = (position: LatLng, howMany: number = 1, blacklist: string[] = []): string[] => { + const distances: Array<{ dist: number; id: string; }> = []; + + Object.keys(window.portals).forEach((portalId) => { + const pos = this.getPortalLocation(portalId); + const dist = this.dist(position, pos); + const isInPlayzone = this.isInPlayzone(pos); + const isNotBlacklisted = !blacklist.includes(portalId) && !this.blacklistedPortals.includes(portalId); + + if (isInPlayzone && isNotBlacklisted) { + distances.push({ + dist, + id: portalId, + }); + } + }); + + return distances + .sort((a, b) => a.dist - b.dist) + .slice(0, howMany) + .map((distInfo) => distInfo.id); + } + + private getPortalLocation = getPortalLocation; +} diff --git a/src/teamShards/visualizeOnIITC.ts b/src/teamShards/visualizeOnIITC.ts new file mode 100644 index 0000000..4333cb9 --- /dev/null +++ b/src/teamShards/visualizeOnIITC.ts @@ -0,0 +1,59 @@ +export default (data) => { + const colors = ['red', 'green', 'orange', 'blue', 'cyan', 'purple', 'turquoise', 'grey', 'dodgerblue']; + const dt = window.plugin.drawTools; + + try { + // clean dt + delete localStorage['plugin-draw-tools-layer']; + dt.drawnItems.clearLayers(); + dt.load(); + + // import new dt + dt.import([ + { + type: 'circle', + latLng: data.targetsMiddlePoint, + radius: data.playzoneRadius, + color: '#bbb' + }, + { + type: 'circle', + latLng: data.targetsMiddlePoint, + radius: data.targetsRadius, + color: '#888' + }, + ...data.teams.map((team, i) => ({ + type: "circle", + latLng: team.from, + radius: 50, + color: colors[i] + })), + ...data.teams.map((team, i) => ({ + type: "circle", + latLng: team.to, + radius: 80, + color: colors[i] + })), + ...data.teams.filter((team) => team.shards.length > 0).map((team, i) => ({ + type: 'marker', + latLng: data.getPortalLocation(team.shards[0].portal), + color: colors[i] + })), + ...data.teams.filter((team) => team.shards.length > 0 && team.shards[0].history.length > 1).map((team, i) => ({ + type: 'polyline', + latLngs: team.shards[0].history.map((portal) => data.getPortalLocation(portal)), + color: colors[i] + })), + ...data.teams.filter((team) => !!team.currentTarget).map((team, i) => ({ + type: "circle", + latLng: data.getPortalLocation(team.currentTarget), + radius: 20, + color: colors[i] + })), + ]); + + dt.save(); + } catch(e) { + console.log('Error when visualizing on iitc', e); + } +} diff --git a/src/userscript.meta.js b/src/userscript.meta.js index 2b89578..0f3a5e5 100644 --- a/src/userscript.meta.js +++ b/src/userscript.meta.js @@ -1,10 +1,10 @@ // ==UserScript== // @id iitc-plugin-mininomaly -// @name IITC plugin: Mininomaly - portal battle -// @version 1.0 -// @description For anomaly-like portal battle event -// @include https://*.ingress.com/intel* -// @include http://*.ingress.com/intel* +// @name IITC plugin: Mininomaly +// @version 2.0 +// @description For anomaly-like events +// @match https://intel.ingress.com/* +// @match http://intel.ingress.com/* // @match https://*.ingress.com/intel* // @match http://*.ingress.com/intel* // @grant none diff --git a/tsconfig.json b/tsconfig.json index 68cb6d8..4b72acb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,12 @@ "checkJs": true, "allowJs": true, "esModuleInterop": true, + "target": "es2017", }, + "lib": [ + "es2017", + "dom" + ], "include": [ "src/**/*", "./index.d.ts",