├── .firebaserc ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ ├── firebase-hosting-merge.yml │ └── firebase-hosting-pull-request.yml ├── .gitignore ├── 404.html ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── favicon.ico ├── firebase.json ├── fonts ├── Quantico400.woff2 ├── Quantico700.woff2 └── Tulpen-One400.woff2 ├── icons ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png └── ms-icon-70x70.png ├── index.html ├── libs ├── font-awesome.min.css └── fontawesome-webfont.woff ├── offline └── index.html ├── public ├── tether_cover.png ├── tether_logo-v2.png ├── tether_logo.png ├── tether_opengraphimage.png └── tether_twittercardimage.png ├── robots.txt ├── service-worker.js ├── sitemap.xml ├── source ├── game.js ├── index.html └── templates │ ├── main.template.html │ └── offline.template.html ├── splashscreens ├── ipad_splash.png ├── ipadpro1_splash.png ├── ipadpro2_splash.png ├── ipadpro3_splash.png ├── iphone5_splash.png ├── iphone6_splash.png ├── iphoneplus_splash.png ├── iphonex_splash.png ├── iphonexr_splash.png └── iphonexsmax_splash.png ├── tether.webmanifest └── tether_theme.mp3 /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "tether-game" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Found a buggo, then use this template to help us out! 4 | title: "[BUG]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | 'on': 6 | push: 7 | branches: 8 | - master 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: FirebaseExtended/action-hosting-deploy@v0 15 | with: 16 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 17 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_TETHER_GAME }}' 18 | channelId: live 19 | projectId: tether-game 20 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | jobs: 7 | build_and_preview: 8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: FirebaseExtended/action-hosting-deploy@v0 13 | with: 14 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 15 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_TETHER_GAME }}' 16 | projectId: tether-game 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # template html files 69 | source/main.template.html 70 | source/offline.template.html -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | rayhanadev@protonmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | Please note the following: 6 | 7 | - We have a code of conduct, please follow it in all your interactions with the project. 8 | - We follow the 'Conventional Commits' commit convention. If your pull request does not adhere to the convention, it will not be merged. 9 | 10 | ## Pull Request Process 11 | 12 | 1. Complete a quick code review for your code, you might catch any errors before you submit. 13 | 2. Make sure your pull request uses the most recent version of the code. 14 | 3. Update the README.md with changes if necessary. This includes any major changes to the features, usage of the package, etc. 15 | 4. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 5. You may merge the Pull Request in once you have the approval of repository maintainer or owner, or if you do not have permission to do that, you may request a reviewer to merge it for you. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ray Arayilakath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **tether!** Swing Around a Ball of Destruction! 2 | 3 | ![demo](https://tether.rayhanadev.repl.co/public/tether_cover.png) 4 | 5 | ### PLAY IT [FULLSCREEN](https://tether.rayhanadev.repl.co) NOW! 6 | 7 | --- 8 | 9 | **tether!** is game where you **wreck as many enemies as possible** using your tether, however if an enemy touches your ball then **you get obliterated**! This game has full mobile and offline support, designed as a progressive web application! This is my group's end-of-the-year project for our CS class. 10 | 11 | ## How to Play 12 | **Click (or tap)** on the ball and **drag** it around! Use the motion of the ball and tether to **destroy each of the enemies**, the *Drifter*, the *Eye*, and the scariest of all... the *Twitchy*. 13 | 14 | ## Features 15 | - **Fast** Load Times 16 | - Polished, Unique Graphics (no third party libs) 17 | - **Vibin'** Background Music 18 | - Full **Mobile Support** 19 | - **Offline Support** 20 | - **Custom** Made Everything 21 | - Progressive Web Application 22 | 23 | ## Tips 24 | The first few levels are tutorials, after a few rounds with each character **you'll enter an endless wave** so be prepared. 25 | 26 | Keep an eye on **enemies spawning in**, they will charge at you **blazing fast** after spawning in! 27 | 28 | **Don't let your tether go wack**! If you move it around too much, it becomes **uncontrollable** and you have an awful time dealing with *Eyes*. 29 | 30 | Instead of charging a *Twitchy* head on, wait for it to **run out of fuel** and then destroy it. 31 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/favicon.ico -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": ".", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fonts/Quantico400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/fonts/Quantico400.woff2 -------------------------------------------------------------------------------- /fonts/Quantico700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/fonts/Quantico700.woff2 -------------------------------------------------------------------------------- /fonts/Tulpen-One400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/fonts/Tulpen-One400.woff2 -------------------------------------------------------------------------------- /icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/apple-icon.png -------------------------------------------------------------------------------- /icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/favicon-16x16.png -------------------------------------------------------------------------------- /icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/favicon-32x32.png -------------------------------------------------------------------------------- /icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/favicon-96x96.png -------------------------------------------------------------------------------- /icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /libs/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('./fontawesome-webfont.woff') format('woff');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} -------------------------------------------------------------------------------- /libs/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/libs/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/tether_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/public/tether_cover.png -------------------------------------------------------------------------------- /public/tether_logo-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/public/tether_logo-v2.png -------------------------------------------------------------------------------- /public/tether_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/public/tether_logo.png -------------------------------------------------------------------------------- /public/tether_opengraphimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/public/tether_opengraphimage.png -------------------------------------------------------------------------------- /public/tether_twittercardimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/public/tether_twittercardimage.png -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # Group 1 2 | User-agent: Googlebot 3 | Disallow: /.github 4 | Disallow: /fonts 5 | Disallow: /icons 6 | Disallow: /libs 7 | Disallow: /public 8 | Disallow: /source 9 | Disallow: /splashscreens 10 | Disallow: /bgm.mp3 11 | Disallow: /CODE_OF_CONDUCT.md 12 | Disallow: /LICENSE 13 | Disallow: /README.md 14 | 15 | # Group 2 16 | User-agent: * 17 | Disallow: /.github 18 | Disallow: /fonts 19 | Disallow: /icons 20 | Disallow: /libs 21 | Disallow: /public 22 | Disallow: /source 23 | Disallow: /splashscreens 24 | Disallow: /bgm.mp3 25 | Disallow: /CODE_OF_CONDUCT.md 26 | Disallow: /LICENSE 27 | Disallow: /README.md 28 | 29 | Sitemap: http://tether.rayhanadev.repl.co/sitemap.xml -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'tether_cache-v2'; 2 | const precacheResources = [ 3 | '/', 4 | '/offline/', 5 | '/404.html', 6 | '/fonts/Quantico400.woff2', 7 | '/fonts/Quantico700.woff2', 8 | '/fonts/Tulpen-One400.woff2', 9 | '/icons/favicon-16x16.png', 10 | '/tether_theme.mp3', 11 | '/libs/font-awesome.min.css', 12 | '/libs/fontawesome-webfont.woff' 13 | ]; 14 | 15 | // When the service worker is installing, open the cache and add the precache resources to it 16 | self.addEventListener('install', (event) => { 17 | console.log('Service worker install event!'); 18 | event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(precacheResources))); 19 | }); 20 | 21 | self.addEventListener('activate', (event) => { 22 | console.log('Service worker activate event!'); 23 | }); 24 | 25 | // When there's an incoming fetch request, try and respond with a precached resource, otherwise fall back to the network 26 | self.addEventListener('fetch', (event) => { 27 | event.respondWith( 28 | caches.match(event.request).then((cachedResponse) => { 29 | if (cachedResponse) { 30 | return cachedResponse; 31 | } 32 | return fetch(event.request); 33 | }), 34 | ); 35 | }); -------------------------------------------------------------------------------- /sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://tether.rayhanadev.repl.co/ 12 | 2021-05-22T16:21:43+00:00 13 | 14 | 15 | https://tether.rayhanadev.repl.co/robots.txt 16 | 2021-05-22T16:21:43+00:00 17 | 18 | 19 | https://tether.rayhanadev.repl.co/manifest.json 20 | 2021-05-22T16:21:43+00:00 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /source/game.js: -------------------------------------------------------------------------------- 1 | document.body.classList.add('game'); 2 | 3 | var storage = (function () { 4 | var uid = new Date; 5 | var storage; 6 | var result; 7 | try { 8 | (storage = window.localStorage).setItem(uid, uid); 9 | result = storage.getItem(uid) == uid; 10 | storage.removeItem(uid); 11 | return result && storage; 12 | } catch (exception) { } 13 | storage = function () { console.log('localStorage Disabled.') }; 14 | storage.getItem = function () { console.log('localStorage Disabled.') }; 15 | storage.setItem = function () { console.log('localStorage Disabled.') }; 16 | return storage; 17 | }()); 18 | 19 | this.top.location !== this.location && (this.top.location = this.location); 20 | 21 | var DEBUG = window.location.hash === '#DEBUG', 22 | INFO = DEBUG || window.location.hash === '#INFO', 23 | game, 24 | music, 25 | canvas, 26 | ctx, 27 | devicePixelRatio = window.devicePixelRatio || 1, 28 | width = window.innerWidth, 29 | height = window.innerHeight, 30 | muteButtonPosition, 31 | muteButtonProximityThreshold = 30, 32 | playButtonPosition, 33 | playButtonProximityThreshold = 30, 34 | maximumPossibleDistanceBetweenTwoMasses, 35 | highScoreCookieKey = 'tetherHighScore', 36 | highScore = storage.getItem(highScoreCookieKey) ?? 0, 37 | musicMutedCookieKey = 'tetherMusicMuted', 38 | lastDayCookieKey = 'tetherLastDate', 39 | streakCountCookieKey = 'tetherStreakCount', 40 | streakCount = storage.getItem(streakCountCookieKey) ?? 0, 41 | subtitleText = "", 42 | lastDate = new Date(Number(storage.getItem(lastDayCookieKey))), 43 | lastTouchStart, 44 | uidCookieKey = 'tetherId', 45 | uid, 46 | playerRGB = [20, 20, 200], 47 | hslVal = 0, 48 | paused = false, 49 | shouldUnmuteImmediately = false, 50 | cookieExpiryDate = new Date(); 51 | 52 | if (window.location.pathname === '/source/') subtitleText = 'Source Development Mode. #OpenSource'; 53 | else subtitleText = 'Swing around a ball and cause pure destruction.'; 54 | 55 | window.addEventListener('offline', () => { 56 | window.location.href = '/offline/'; 57 | }); 58 | 59 | cookieExpiryDate.setFullYear(cookieExpiryDate.getFullYear() + 50); 60 | var cookieSuffix = '; expires=' + cookieExpiryDate.toUTCString(); 61 | 62 | function extend(base, sub) { 63 | sub.prototype = Object.create(base.prototype); 64 | sub.prototype.constructor = sub; 65 | Object.defineProperty(sub.prototype, 'constructor', { 66 | enumerable: false, 67 | value: sub, 68 | }); 69 | } 70 | 71 | function choice(array) { 72 | return array[Math.floor(Math.random() * array.length)]; 73 | } 74 | 75 | function somewhereInTheViewport() { 76 | return { 77 | x: Math.random() * width, 78 | y: Math.random() * height, 79 | }; 80 | } 81 | 82 | function somewhereJustOutsideTheViewport(buffer) { 83 | var somewhere = somewhereInTheViewport(); 84 | var edgeSeed = Math.random(); 85 | 86 | if (edgeSeed < 0.25) somewhere.x = -buffer; 87 | else if (edgeSeed < 0.5) somewhere.x = width + buffer; 88 | else if (edgeSeed < 0.75) somewhere.y = -buffer; 89 | else somewhere.y = height + buffer; 90 | 91 | return somewhere; 92 | } 93 | 94 | function closestWithinViewport(position) { 95 | var newPos = { x: position.x, y: position.y }; 96 | newPos = forXAndY([newPos, { x: 0, y: 0 }], forXAndY.theGreater); 97 | newPos = forXAndY([newPos, { x: width, y: height }], forXAndY.theLesser); 98 | return newPos; 99 | } 100 | 101 | function getAttributeFromAllObjs(objs, attr) { 102 | var attrs = []; 103 | for (var i = 0; i < objs.length; i++) { 104 | attrs.push(objs[i][attr]); 105 | } 106 | return attrs; 107 | } 108 | 109 | function forXAndY(objs, func) { 110 | return { 111 | x: func.apply(null, getAttributeFromAllObjs(objs, 'x')), 112 | y: func.apply(null, getAttributeFromAllObjs(objs, 'y')), 113 | }; 114 | } 115 | 116 | forXAndY.aPlusHalfB = function (a, b) { 117 | return a + b * 5; 118 | }; 119 | forXAndY.aPlusBTimesSpeed = function (a, b) { 120 | return a + b * game.timeDelta; 121 | }; 122 | forXAndY.subtract = function (a, b) { 123 | return a - b; 124 | }; 125 | forXAndY.invSubtract = function (a, b) { 126 | return b - a; 127 | }; 128 | forXAndY.theGreater = function (a, b) { 129 | return a > b ? a : b; 130 | }; 131 | forXAndY.theLesser = function (a, b) { 132 | return a < b ? a : b; 133 | }; 134 | forXAndY.add = function () { 135 | var s = 0; 136 | for (var i = 0; i < arguments.length; i++) s += arguments[i]; 137 | return s; 138 | }; 139 | forXAndY.multiply = function () { 140 | var p = 1; 141 | for (var i = 0; i < arguments.length; i++) p *= arguments[i]; 142 | return p; 143 | }; 144 | 145 | function randomisedVector(vector, potentialMagnitude) { 146 | var angle = Math.random() * Math.PI * 2; 147 | var magnitude = Math.random() * potentialMagnitude; 148 | return forXAndY([vector, vectorAt(angle, magnitude)], forXAndY.add); 149 | } 150 | 151 | function getIntersection(line1, line2) { 152 | var denominator, 153 | a, 154 | b, 155 | numerator1, 156 | numerator2, 157 | result = { 158 | x: null, 159 | y: null, 160 | onLine1: false, 161 | onLine2: false, 162 | }; 163 | 164 | denominator = 165 | (line2[1].y - line2[0].y) * (line1[1].x - line1[0].x) - 166 | (line2[1].x - line2[0].x) * (line1[1].y - line1[0].y); 167 | 168 | if (denominator === 0) { 169 | return result; 170 | } 171 | 172 | a = line1[0].y - line2[0].y; 173 | b = line1[0].x - line2[0].x; 174 | numerator1 = (line2[1].x - line2[0].x) * a - (line2[1].y - line2[0].y) * b; 175 | numerator2 = (line1[1].x - line1[0].x) * a - (line1[1].y - line1[0].y) * b; 176 | a = numerator1 / denominator; 177 | b = numerator2 / denominator; 178 | 179 | result.x = line1[0].x + a * (line1[1].x - line1[0].x); 180 | result.y = line1[0].y + a * (line1[1].y - line1[0].y); 181 | 182 | if (a > 0 && a < 1) { 183 | result.onLine1 = true; 184 | } 185 | if (b > 0 && b < 1) { 186 | result.onLine2 = true; 187 | } 188 | return result; 189 | } 190 | 191 | function pointInPolygon(point, polygon) { 192 | var i, j; 193 | var c = 0; 194 | var numberOfPoints = polygon.length; 195 | for (i = 0, j = numberOfPoints - 1; i < numberOfPoints; j = i++) { 196 | if ( 197 | ((polygon[i].y <= point.y && point.y < polygon[j].y) || 198 | (polygon[j].y <= point.y && point.y < polygon[i].y)) && 199 | point.x < 200 | ((polygon[j].x - polygon[i].x) * (point.y - polygon[i].y)) / 201 | (polygon[j].y - polygon[i].y) + 202 | polygon[i].x 203 | ) { 204 | c = !c; 205 | } 206 | } 207 | 208 | return c; 209 | } 210 | 211 | function vectorMagnitude(vector) { 212 | return Math.abs( 213 | Math.pow(Math.pow(vector.x, 2) + Math.pow(vector.y, 2), 1 / 2), 214 | ); 215 | } 216 | 217 | function vectorAngle(vector) { 218 | theta = Math.atan(vector.y / vector.x); 219 | if (vector.x < 0) theta += Math.PI; 220 | return theta; 221 | } 222 | 223 | function vectorAt(angle, magnitude) { 224 | return { 225 | x: Math.cos(angle) * magnitude, 226 | y: Math.sin(angle) * magnitude, 227 | }; 228 | } 229 | 230 | function inverseVector(vector) { 231 | var angle = vectorAngle(vector); 232 | var mag = vectorMagnitude(vector); 233 | return vectorAt(angle, 1 / mag); 234 | } 235 | 236 | function linesFromPolygon(polygon) { 237 | var polyLine = []; 238 | for (var i = 1; i < polygon.length; i++) { 239 | polyLine.push([polygon[i - 1], polygon[i]]); 240 | } 241 | return polyLine; 242 | } 243 | 244 | function lineAngle(line) { 245 | return vectorAngle({ 246 | x: line[1].x - line[0].x, 247 | y: line[1].y - line[0].y, 248 | }); 249 | } 250 | 251 | function lineDelta(line) { 252 | return forXAndY(line, forXAndY.invSubtract); 253 | } 254 | 255 | function rgbWithOpacity(rgb, opacity) { 256 | var rgbStrings = []; 257 | for (var i = 0; i < rgb.length; rgbStrings.push(rgb[i++].toFixed(0))); 258 | return 'rgba(' + rgbStrings.join(',') + ',' + opacity.toFixed(2) + ')'; 259 | } 260 | 261 | function hsl(hsl) { 262 | return 'hsl(' + hsl + ', 100%, 50%)'; 263 | } 264 | 265 | function draw(opts) { 266 | for (var defaultKey in draw.defaults) { 267 | if (!(defaultKey in opts)) opts[defaultKey] = draw.defaults[defaultKey]; 268 | } 269 | 270 | if (DEBUG) { 271 | for (var key in opts) { 272 | if (!(key in draw.defaults)) throw key + ' is not a valid option to draw()'; 273 | } 274 | } 275 | 276 | ctx.fillStyle = opts.fillStyle; 277 | ctx.strokeStyle = opts.strokeStyle; 278 | ctx.lineWidth = opts.lineWidth; 279 | 280 | ctx.beginPath(); 281 | 282 | if (opts.type === 'arc') draw.arc(opts); 283 | else if (opts.type === 'line') draw.line(opts); 284 | else if (opts.type === 'text') draw.text(opts); 285 | else if (opts.type === 'rect') draw.rect(opts); 286 | else if (opts.type === 'clear') draw.clear(opts); 287 | else throw opts.type + ' is not an implemented draw type'; 288 | 289 | if (opts.fill) ctx.fill(); 290 | if (opts.stroke) ctx.stroke(); 291 | } 292 | 293 | draw.defaults = { 294 | type: null, 295 | fill: false, 296 | stroke: false, 297 | 298 | linePaths: [], 299 | 300 | arcCenter: undefined, 301 | arcRadius: 0, 302 | arcStart: 0, 303 | arcFinish: 2 * Math.PI, 304 | 305 | text: '', 306 | textPosition: undefined, 307 | fontFamily: 'Tulpen One', 308 | fontFallback: 'sans-serif', 309 | textAlign: 'center', 310 | textBaseline: 'middle', 311 | fontSize: 20, 312 | 313 | rectBounds: [], 314 | 315 | lineWidth: 1, 316 | fillStyle: '#000', 317 | strokeStyle: '#000', 318 | }; 319 | 320 | draw.arc = function (opts) { 321 | ctx.arc( 322 | opts.arcCenter.x, 323 | opts.arcCenter.y, 324 | opts.arcRadius, 325 | opts.arcStart, 326 | opts.arcFinish, 327 | ); 328 | }; 329 | 330 | draw.line = function (opts) { 331 | for (var ipath = 0; ipath < opts.linePaths.length; ipath++) { 332 | var path = opts.linePaths[ipath]; 333 | 334 | ctx.moveTo(path[0].x, path[0].y); 335 | 336 | for (var ipos = 1; ipos < path.length; ipos++) { 337 | var position = path[ipos]; 338 | ctx.lineTo(position.x, position.y); 339 | } 340 | } 341 | }; 342 | 343 | draw.rect = function (opts) { 344 | ctx.fillRect.apply(ctx, opts.rectBounds); 345 | }; 346 | 347 | draw.text = function (opts) { 348 | ctx.font = 349 | opts.fontSize.toString() + 350 | 'px "' + 351 | opts.fontFamily + 352 | '", ' + 353 | opts.fontFallback; 354 | ctx.textAlign = opts.textAlign; 355 | ctx.textBaseline = opts.textBaseline; 356 | 357 | ctx.fillText(opts.text, opts.textPosition.x, opts.textPosition.y); 358 | }; 359 | 360 | draw.clear = function () { 361 | ctx.clearRect(0, 0, width, height); 362 | }; 363 | 364 | function scaleCanvas(ratio) { 365 | canvas.width = width * ratio; 366 | canvas.height = height * ratio; 367 | 368 | ctx.scale(ratio, ratio); 369 | } 370 | 371 | var achievements = { 372 | die: { 373 | name: "You're coming with me", 374 | description: 'Take solace in your mutual destruction', 375 | }, 376 | introduction: { 377 | name: 'How to play', 378 | description: 'Die with one point', 379 | }, 380 | kill: { 381 | name: 'Weapon of choice', 382 | description: 'Kill an enemy without dying yourself', 383 | }, 384 | impact: { 385 | name: 'Concussion', 386 | description: 'Feel the impact', 387 | }, 388 | quickdraw: { 389 | name: 'Quick draw', 390 | description: 'Kill an enemy within a few moments of it spawning', 391 | }, 392 | omnicide: { 393 | name: 'Omnicide', 394 | description: 'Kill every type of enemy in one game', 395 | }, 396 | panic: { 397 | name: 'Panic', 398 | description: 'Be alive while fifteen enemies are on screen', 399 | }, 400 | lowRes: { 401 | name: 'Cramped', 402 | description: 403 | 'Score ten points at 500x500px or less (currently ' + 404 | width + 405 | 'x' + 406 | height + 407 | ')', 408 | }, 409 | handsFree: { 410 | name: 'Hands-free', 411 | description: 'Score five points in a row without moving the tether', 412 | }, 413 | }; 414 | 415 | function initCanvas() { 416 | var later24Hours = lastDate.getTime() + 86400000; 417 | var later48Hours = lastDate.getTime() + 2 * 86400000; 418 | var currentDate = new Date(); 419 | 420 | var streak = Number(storage.getItem(streakCountCookieKey)); 421 | 422 | if ( 423 | !Number(storage.getItem(lastDayCookieKey)) || 424 | Number.isNaN(lastDate) 425 | ) { 426 | saveCookie(lastDayCookieKey, currentDate.getTime()); 427 | saveCookie(streakCountCookieKey, 0); 428 | } else if ( 429 | later48Hours > Number(new Date()) && 430 | Number(new Date()) > later24Hours 431 | ) { 432 | saveCookie(streakCountCookieKey, (streak += 1)); 433 | saveCookie(lastDayCookieKey, currentDate.getTime()); 434 | } else if (Number(new Date()) < later24Hours) { 435 | } else { 436 | saveCookie(streakCountCookieKey, 0); 437 | saveCookie(lastDayCookieKey, currentDate.getTime()); 438 | } 439 | 440 | switch (streak) { 441 | case 0: 442 | break; 443 | case 1: 444 | playerRGB = [206, 125, 165]; 445 | break; 446 | case 2: 447 | playerRGB = [50, 147, 165]; 448 | break; 449 | case 3: 450 | playerRGB = [223, 41, 53]; 451 | break; 452 | case 4: 453 | playerRGB = [223, 41, 53]; 454 | break; 455 | case 5: 456 | playerRGB = [39, 38, 53]; 457 | break; 458 | case 6: 459 | playerRGB = [255, 231, 76]; 460 | break; 461 | case 7: 462 | case 8: 463 | case 9: 464 | playerRGB = [15, 14, 14]; 465 | break; 466 | default: 467 | case 10: 468 | playerRGB = 'Rainbow'; 469 | console.log('Congrats on your 10 day streak!!'); 470 | break; 471 | } 472 | 473 | width = window.innerWidth; 474 | height = window.innerHeight; 475 | muteButtonPosition = { x: 32, y: height - 28 }; 476 | playButtonPosition = { x: 32, y: height - 28 }; 477 | 478 | maximumPossibleDistanceBetweenTwoMasses = vectorMagnitude({ 479 | x: width, 480 | y: height, 481 | }); 482 | 483 | canvas = document.getElementById('game'); 484 | ctx = canvas.getContext('2d'); 485 | 486 | canvas.style.width = width.toString() + 'px'; 487 | canvas.style.height = height.toString() + 'px'; 488 | 489 | canvas.requestPointerLock = 490 | canvas.requestPointerLock || canvas.mozRequestPointerLock; 491 | document.exitPointerLock = 492 | document.exitPointerLock || document.mozExitPointerLock; 493 | 494 | for (var key in storage) { 495 | var value = storage.getItem(key); 496 | if ( 497 | achievements[key] || 498 | key === musicMutedCookieKey || 499 | key === highScoreCookieKey 500 | ) { 501 | saveCookie(key, value); 502 | if (achievements[key]) { 503 | achievements[key].unlocked = new Date(Number(value)); 504 | } 505 | } 506 | } 507 | 508 | scaleCanvas(devicePixelRatio); 509 | } 510 | 511 | window.addEventListener('resize', function (event) { 512 | canvas = document.getElementById('game'); 513 | 514 | width = window.innerWidth; 515 | height = window.innerHeight; 516 | maximumPossibleDistanceBetweenTwoMasses = vectorMagnitude({ 517 | x: width, 518 | y: height, 519 | }); 520 | muteButtonPosition = { x: 32, y: height - 28 }; 521 | playButtonPosition = { x: 32, y: height - 28 }; 522 | devicePixelRatio = window.devicePixelRatio || 1; 523 | 524 | canvas.style.width = width + 'px'; 525 | canvas.style.height = height + 'px'; 526 | 527 | if (!game.started) { 528 | game.tether.teleportTo({ 529 | x: width / 2, 530 | y: (height / 3) * 2, 531 | }); 532 | } 533 | scaleCanvas(devicePixelRatio); 534 | }); 535 | 536 | function timeToNextClaim() { 537 | var deadline = lastDate.getTime() + 86400000; 538 | var timeRemaining = deadline - new Date(); 539 | var formattedTime = new Date(timeRemaining); 540 | 541 | if (formattedTime > 0) { 542 | return `${ 543 | formattedTime.getHours() > 9 ? '' : '0' 544 | }${formattedTime.getHours()}:${ 545 | formattedTime.getMinutes() > 9 ? '' : '0' 546 | }${formattedTime.getMinutes()}:${ 547 | formattedTime.getSeconds() > 9 ? '' : '0' 548 | }${formattedTime.getSeconds()}`; 549 | } else { 550 | return 'Right Now!'; 551 | } 552 | } 553 | 554 | function edgesOfCanvas() { 555 | return linesFromPolygon([ 556 | { x: 0, y: 0 }, 557 | { x: 0, y: height }, 558 | { x: width, y: height }, 559 | { x: width, y: 0 }, 560 | { x: 0, y: 0 }, 561 | ]); 562 | } 563 | 564 | initCanvas(); 565 | 566 | function Music() { 567 | var self = this, 568 | path; 569 | 570 | if (INFO) path = '../tether_theme.mp3'; 571 | else path = '../tether_theme.mp3'; 572 | 573 | self.element = new Audio(path); 574 | 575 | if (typeof self.element.loop === 'boolean') { 576 | if (INFO) console.log('using element.loop for looping'); 577 | self.element.loop = true; 578 | } else { 579 | if (INFO) console.log('using event listener for looping'); 580 | self.element.addEventListener('ended', function () { 581 | self.element.currentTime = 0; 582 | }); 583 | } 584 | 585 | self.timeSignature = 4; 586 | 587 | if (shouldUnmuteImmediately) self.element.play(); 588 | } 589 | 590 | Music.prototype = { 591 | bpm: 90, 592 | url: 'tether_theme.mp3', 593 | delayCompensation: 0.03, 594 | 595 | totalBeat: function () { 596 | return ((this.element.currentTime + this.delayCompensation) / 60) * this.bpm; 597 | }, 598 | 599 | measure: function () { 600 | return this.totalBeat() / this.timeSignature; 601 | }, 602 | 603 | beat: function () { 604 | return music.totalBeat() % this.timeSignature; 605 | }, 606 | 607 | timeSinceBeat: function () { 608 | return this.beat() % 1; 609 | }, 610 | }; 611 | 612 | function Mass() { 613 | this.seed = Math.random(); 614 | } 615 | 616 | Mass.prototype = { 617 | position: { x: 0, y: 0 }, 618 | positionOnPreviousFrame: { x: 0, y: 0 }, 619 | velocity: { x: 0, y: 0 }, 620 | force: { x: 0, y: 0 }, 621 | mass: 1, 622 | lubricant: 1, 623 | radius: 0, 624 | visibleRadius: null, 625 | dashInterval: 1 / 8, 626 | walls: false, 627 | bounciness: 0, 628 | rgb: [60, 60, 60], 629 | reactsToForce: true, 630 | 631 | journeySincePreviousFrame: function () { 632 | return [this.positionOnPreviousFrame, this.position]; 633 | }, 634 | 635 | bounceInDimension: function (d, max) { 636 | var distanceFromFarEdge = max - this.radius - this.position[d]; 637 | var distanceFromNearEdge = this.position[d] - this.radius; 638 | 639 | if (distanceFromNearEdge < 0) { 640 | this.velocity[d] *= -this.bounciness; 641 | this.position[d] = distanceFromNearEdge * this.bounciness + this.radius; 642 | this.bounceCallback(); 643 | } else if (distanceFromFarEdge < 0) { 644 | this.velocity[d] *= -this.bounciness; 645 | this.position[d] = max - distanceFromFarEdge * this.bounciness - this.radius; 646 | this.bounceCallback(); 647 | } 648 | }, 649 | 650 | bounceCallback: function () { }, 651 | 652 | collideWithWalls: function () { 653 | if (!this.walls) return; 654 | this.bounceInDimension('x', width); 655 | this.bounceInDimension('y', height); 656 | }, 657 | 658 | setPosition: function (position) { 659 | this.positionOnPreviousFrame = this.position; 660 | this.position = position; 661 | }, 662 | 663 | teleportTo: function (position) { 664 | this.positionOnPreviousFrame = position; 665 | this.position = position; 666 | }, 667 | 668 | reactToVelocity: function () { 669 | this.setPosition( 670 | forXAndY([this.position, this.velocity], forXAndY.aPlusBTimesSpeed), 671 | ); 672 | this.collideWithWalls(); 673 | }, 674 | 675 | velocityDelta: function () { 676 | var self = this; 677 | return forXAndY([this.force], function (force) { 678 | return force / self.mass; 679 | }); 680 | }, 681 | 682 | reactToForce: function () { 683 | var self = this; 684 | var projectedVelocity = forXAndY( 685 | [this.velocity, this.velocityDelta()], 686 | forXAndY.aPlusBTimesSpeed, 687 | ); 688 | 689 | this.velocity = forXAndY([projectedVelocity], function (projected) { 690 | return projected * Math.pow(self.lubricant, game.timeDelta); 691 | }); 692 | 693 | this.reactToVelocity(); 694 | }, 695 | 696 | step: function () { 697 | if (this.reactsToForce) this.reactToForce(); 698 | }, 699 | 700 | getOpacity: function () { 701 | var opacity; 702 | if (!this.died) opacity = 1; 703 | else opacity = 1 / Math.max(1, game.timeElapsed - this.died); 704 | return opacity; 705 | }, 706 | 707 | getCurrentColor: function () { 708 | if (this.rgb === 'Rainbow') { 709 | if (hslVal !== 360) hslVal++; 710 | else hslVal = 0; 711 | } 712 | 713 | return this.rgb === 'Rainbow' 714 | ? hsl(hslVal) 715 | : rgbWithOpacity(this.rgb, this.getOpacity()); 716 | }, 717 | 718 | draw: function () { 719 | var radius = this.radius; 720 | if (this.visibleRadius !== null) radius = this.visibleRadius; 721 | 722 | draw({ 723 | type: 'arc', 724 | arcRadius: radius, 725 | arcCenter: this.position, 726 | fillStyle: this.getCurrentColor(), 727 | fill: true, 728 | }); 729 | }, 730 | 731 | drawDottedOutline: function () { 732 | for (var i = 0; i < 1; i += this.dashInterval) { 733 | var startAngle = game.timeElapsed / 100 + i * Math.PI * 2; 734 | draw({ 735 | type: 'arc', 736 | stroke: true, 737 | strokeStyle: this.getCurrentColor(), 738 | arcCenter: this.position, 739 | arcStart: startAngle, 740 | arcFinish: startAngle + Math.PI * this.dashInterval * 0.7, 741 | arcRadius: this.radius, 742 | }); 743 | } 744 | }, 745 | 746 | explode: function () { 747 | for (i = 0; i < 50; i++) { 748 | var angle = Math.random() * Math.PI * 2; 749 | var magnitude = Math.random() * 40; 750 | var velocity = forXAndY( 751 | [vectorAt(angle, magnitude), this.velocity], 752 | forXAndY.add, 753 | ); 754 | new FireParticle(this.position, velocity); 755 | } 756 | }, 757 | 758 | focusSegment: function (offset) { 759 | var baseAngle = game.timeElapsed / 30 + Math.cos(game.timeElapsed / 10) * 0.2; 760 | 761 | draw({ 762 | type: 'arc', 763 | stroke: true, 764 | arcCenter: this.position, 765 | arcStart: baseAngle + offset, 766 | arcFinish: baseAngle + Math.PI * 0.5 + offset, 767 | arcRadius: 40 + Math.sin(game.timeElapsed / 10) * 10, 768 | strokeStyle: rgbWithOpacity([0, 0, 0], 0.6), 769 | }); 770 | }, 771 | 772 | focus: function () { 773 | this.focusSegment(0); 774 | this.focusSegment(Math.PI); 775 | }, 776 | }; 777 | 778 | function BackgroundPart(i) { 779 | Mass.call(this); 780 | this.i = i; 781 | this.baseRadius = (2 * Math.max(width, height)) / i; 782 | this.radius = 1; 783 | this.bounciness = 1; 784 | this.velocity = vectorAt(Math.PI * 2 * Math.random(), i * Math.random()); 785 | this.teleportTo(somewhereInTheViewport()); 786 | this.walls = true; 787 | } 788 | extend(Mass, BackgroundPart); 789 | 790 | BackgroundPart.prototype.getCurrentColor = function () { 791 | return this.color; 792 | }; 793 | 794 | BackgroundPart.prototype.step = function () { 795 | this.color = rgbWithOpacity([127, 127, 127], 0.005 * this.i); 796 | 797 | if (game.clickShouldMute && music.element.paused) { 798 | this.color = rgbWithOpacity([255, 255, 255], 0.05 * this.i); 799 | this.visibleRadius = this.baseRadius + Math.random() * this.baseRadius; 800 | } else if (!music.element.paused) { 801 | this.visibleRadius = (1 / music.timeSinceBeat()) * 20 + this.baseRadius; 802 | } else { 803 | this.visibleRadius = this.baseRadius; 804 | } 805 | 806 | Mass.prototype.step.call(this); 807 | }; 808 | 809 | function Background() { 810 | this.parts = []; 811 | for (var i = 0; i < 10; i++) { 812 | this.parts.push(new BackgroundPart(i)); 813 | } 814 | } 815 | 816 | Background.prototype.draw = function () { 817 | if (game.clickShouldMute && music.element.paused) { 818 | draw({ 819 | type: 'rect', 820 | rectBounds: [0, 0, width, height], 821 | fillStyle: rgbWithOpacity([0, 0, 0], 1), 822 | }); 823 | } 824 | 825 | for (var i = 0; i < this.parts.length; this.parts[i++].draw()); 826 | }; 827 | 828 | Background.prototype.step = function () { 829 | for (var i = 0; i < this.parts.length; this.parts[i++].step()); 830 | }; 831 | 832 | function Tether() { 833 | Mass.call(this); 834 | this.radius = 5; 835 | 836 | this.locked = true; 837 | this.unlockable = true; 838 | this.rgb = playerRGB ?? [20, 20, 200]; 839 | 840 | this.teleportTo({ 841 | x: width / 2, 842 | y: (height / 3) * 2, 843 | }); 844 | 845 | this.lastInteraction = null; 846 | this.pointsScoredSinceLastInteraction = 0; 847 | 848 | var self = this; 849 | 850 | document.addEventListener('mousemove', function (e) { 851 | if ( 852 | self.lastInteraction === 'mouse' && 853 | document.pointerLockElement !== canvas 854 | ) 855 | game.lastMousePosition = { x: e.layerX, y: e.layerY }; 856 | self.lastInteraction = 'mouse'; 857 | }); 858 | 859 | document.addEventListener('touchend', function (e) { 860 | self.locked = true; 861 | }); 862 | 863 | function exitTether() { 864 | if ( 865 | document.pointerLockElement === canvas || 866 | document.mozPointerLockElement === canvas 867 | ) 868 | self.locked = false; 869 | else self.locked = true; 870 | } 871 | 872 | if ('onpointerlockchange' in document) 873 | document.addEventListener('pointerlockchange', exitTether); 874 | else if ('onmozpointerlockchange' in document) 875 | document.addEventListener('mozpointerlockchange', exitTether); 876 | 877 | function handleTouch(e) { 878 | e.preventDefault(); 879 | self.lastInteraction = 'touch'; 880 | if (document.pointerLockElement) document.exitPointerLock(); 881 | touch = e.changedTouches[0]; 882 | game.lastMousePosition = { x: touch.clientX, y: touch.clientY }; 883 | } 884 | 885 | document.addEventListener('touchstart', handleTouch, { passive: false }); 886 | document.addEventListener('touchmove', handleTouch, { passive: false }); 887 | 888 | return this; 889 | } 890 | extend(Mass, Tether); 891 | 892 | Tether.prototype.setPosition = function (position) { 893 | if (this.lastInteraction !== 'mouse' || document.pointerLockElement === canvas) 894 | Mass.prototype.setPosition.call(this, position); 895 | if (this.position !== this.positionOnPreviousFrame) { 896 | this.pointsScoredSinceLastInteraction = 0; 897 | } 898 | }; 899 | 900 | Tether.prototype.step = function () { 901 | var leniency = this.lastInteraction === 'touch' ? 50 : 30; 902 | 903 | if ( 904 | this.unlockable && 905 | vectorMagnitude( 906 | forXAndY([this.position, game.lastMousePosition], forXAndY.subtract), 907 | ) < leniency 908 | ) { 909 | if (canvas.requestPointerLock) canvas.requestPointerLock(); 910 | if ( 911 | !(this.lastInteraction !== 'mouse' || document.pointerLockElement === canvas) 912 | ) 913 | return; 914 | 915 | this.locked = false; 916 | 917 | if (!game.started) { 918 | game.start(); 919 | } 920 | } 921 | 922 | if (!this.locked) { 923 | this.setPosition(closestWithinViewport(game.lastMousePosition)); 924 | } else { 925 | this.setPosition(this.position); 926 | } 927 | }; 928 | 929 | Tether.prototype.draw = function () { 930 | if (this.locked && this.unlockable) this.focus(); 931 | Mass.prototype.draw.call(this); 932 | }; 933 | 934 | function Player(tether) { 935 | Mass.call(this); 936 | this.mass = 50; 937 | this.onceGameHasStartedLubricant = 0.99; 938 | this.lubricant = 1; 939 | this.radius = 10; 940 | this.walls = true; 941 | this.teleportTo({ 942 | x: Math.min((width / 10) * 9, width / 2 + 200), 943 | y: 5 * (height / 9), 944 | }); 945 | this.velocity = { x: 0, y: -height / 80 }; 946 | this.bounciness = 0.4; 947 | 948 | this.tether = tether; 949 | this.rgb = playerRGB ?? [20, 20, 200]; 950 | } 951 | extend(Mass, Player); 952 | 953 | Player.prototype.step = function () { 954 | this.force = forXAndY( 955 | [this.tether.position, this.position], 956 | forXAndY.subtract, 957 | ); 958 | Mass.prototype.step.call(this); 959 | }; 960 | 961 | function Cable(tether, player) { 962 | var self = this; 963 | 964 | self.areaCoveredThisStep = function () { 965 | return [ 966 | tether.positionOnPreviousFrame, 967 | player.positionOnPreviousFrame, 968 | player.position, 969 | tether.position, 970 | ]; 971 | }; 972 | 973 | self.line = function () { 974 | return [tether.position, player.position]; 975 | }; 976 | 977 | self.draw = function () { 978 | draw({ 979 | type: 'line', 980 | stroke: true, 981 | strokeStyle: `${ 982 | playerRGB === 'Rainbow' 983 | ? `${hsl(hslVal)}` 984 | : `rgba(${playerRGB[0] ?? 20}, ${playerRGB[1] ?? 20}, ${ 985 | playerRGB[2] ?? 200 986 | }, 1)` 987 | }`, 988 | linePaths: [self.line()], 989 | }); 990 | 991 | if (DEBUG) self.drawAreaCoveredThisStep(); 992 | }; 993 | 994 | self.drawAreaCoveredThisStep = function () { 995 | draw({ 996 | type: 'line', 997 | fill: true, 998 | fillStyle: rgbWithOpacity([127, 127, 255], 0.3), 999 | linePaths: [self.areaCoveredThisStep()], 1000 | }); 1001 | }; 1002 | } 1003 | 1004 | function Enemy(opts) { 1005 | Mass.call(this); 1006 | this.died = null; 1007 | this.exhausts = []; 1008 | this.spawned = false; 1009 | 1010 | this.spawnAt = opts.spawnAt; 1011 | this.wave = opts.wave; 1012 | this.target = this.getTarget(); 1013 | } 1014 | extend(Mass, Enemy); 1015 | 1016 | Enemy.prototype.getTarget = function () { 1017 | return game.player; 1018 | }; 1019 | 1020 | Enemy.prototype.randomSpawnPosition = function () { 1021 | return somewhereInTheViewport(this.radius); 1022 | }; 1023 | 1024 | Enemy.prototype.getTargetVector = function () { 1025 | return forXAndY([this.target.position, this.position], forXAndY.subtract); 1026 | }; 1027 | 1028 | Enemy.prototype.step = function () { 1029 | if ( 1030 | this.force.x !== 0 && 1031 | this.force.y !== 0 && 1032 | Math.random() < game.timeDelta * vectorMagnitude(this.velocityDelta()) 1033 | ) { 1034 | new Exhaust(this); 1035 | } 1036 | 1037 | Mass.prototype.step.call(this); 1038 | }; 1039 | 1040 | Enemy.prototype.die = function (playerDeservesAchievement) { 1041 | if (this.died) { 1042 | if (INFO) console.log('tried to kill enemy that already died'); 1043 | return; 1044 | } 1045 | if (playerDeservesAchievement) { 1046 | unlockAchievement('kill'); 1047 | 1048 | var name = this.constructor.name; 1049 | 1050 | if (game.enemyTypesKilled.indexOf(name) === -1) { 1051 | game.enemyTypesKilled.push(name); 1052 | if (INFO) console.log(game.enemyTypesKilled); 1053 | if (game.enemyTypesKilled.length === enemyPool.length) { 1054 | unlockAchievement('omnicide'); 1055 | } 1056 | } 1057 | 1058 | if (this.died - this.spawnAt < 5) unlockAchievement('quickdraw'); 1059 | } 1060 | this.explode(); 1061 | this.died = game.timeElapsed; 1062 | if (game.ended) return; 1063 | 1064 | game.incrementScore(1); 1065 | }; 1066 | 1067 | Enemy.prototype.draw = function () { 1068 | if (DEBUG && !this.died) this.drawTargetVector(); 1069 | 1070 | Mass.prototype.draw.call(this); 1071 | }; 1072 | 1073 | Enemy.prototype.drawTargetVector = function () { 1074 | draw({ 1075 | type: 'line', 1076 | stroke: true, 1077 | strokeStyle: rgbWithOpacity([255, 127, 127], 0.7), 1078 | linePaths: [[this.position, this.target.position]], 1079 | }); 1080 | }; 1081 | 1082 | Enemy.prototype.drawWarning = function () { 1083 | var timeUntilSpawn = 1084 | (this.spawnAt - game.timeElapsed) / this.wave.spawnWarningDuration; 1085 | 1086 | draw({ 1087 | type: 'arc', 1088 | stroke: true, 1089 | arcCenter: this.position, 1090 | arcRadius: 1091 | (this.visibleRadius || this.radius) / 2 + Math.pow(timeUntilSpawn, 2) * 700, 1092 | lineWidth: 1093 | ((2 * (this.visibleRadius || this.radius)) / 2) * 1094 | Math.pow(1 - timeUntilSpawn, 3), 1095 | strokeStyle: rgbWithOpacity( 1096 | this.rgbWarning || this.rgb, 1097 | (1 - timeUntilSpawn) * this.getOpacity(), 1098 | ), 1099 | }); 1100 | }; 1101 | 1102 | function Drifter(opts) { 1103 | Enemy.call(this, opts); 1104 | this.radius = 10; 1105 | this.rgb = [30, 150, 150]; 1106 | this.thrustAngle = undefined; 1107 | this.walls = true; 1108 | this.bounciness = 1; 1109 | this.power = 0.3; 1110 | this.lubricant = 0.8; 1111 | this.curvature = Math.max(width, height); 1112 | } 1113 | extend(Enemy, Drifter); 1114 | 1115 | Drifter.prototype.getTarget = function () { 1116 | return game.tether; 1117 | }; 1118 | 1119 | Drifter.prototype.randomSpawnPosition = function () { 1120 | var somewhere = somewhereInTheViewport(); 1121 | somewhere.x = (somewhere.x * 2) / 3 + width / 6; 1122 | somewhere.y = (somewhere.y * 2) / 3 + height / 6; 1123 | return somewhere; 1124 | }; 1125 | 1126 | Drifter.prototype.step = function () { 1127 | if (this.thrustAngle === undefined) { 1128 | this.thrustAngle = vectorAngle(this.getTargetVector()); 1129 | 1130 | var error = Math.random() + 1; 1131 | if (Math.random() > 0.5) error *= -1; 1132 | this.thrustAngle += error / 5; 1133 | } 1134 | 1135 | if (!this.died) { 1136 | this.force = vectorAt(this.thrustAngle, this.power); 1137 | } else this.force = { x: 0, y: 0 }; 1138 | 1139 | Enemy.prototype.step.call(this); 1140 | }; 1141 | 1142 | Drifter.prototype.bounceCallback = function () { 1143 | this.thrustAngle = vectorAngle(this.velocity); 1144 | }; 1145 | 1146 | function Eye(opts) { 1147 | Enemy.call(this, opts); 1148 | 1149 | var size = opts.size || 0.75 + Math.random() / 1.5; 1150 | 1151 | this.mass = size * (1500 / maximumPossibleDistanceBetweenTwoMasses); 1152 | 1153 | this.lubricant = 0.9; 1154 | this.radius = size * 10; 1155 | this.shadowRadius = this.radius + 3; 1156 | this.shadowOpacity = 0.5; 1157 | this.rgb = [255, 255, 255]; 1158 | this.rgbWarning = [50, 50, 50]; 1159 | } 1160 | extend(Enemy, Eye); 1161 | 1162 | Eye.prototype.step = function () { 1163 | if (!this.died) { 1164 | var targetVector = this.getTargetVector(); 1165 | targetVectorMagnitude = vectorMagnitude(targetVector); 1166 | this.force = forXAndY([targetVector], function (target) { 1167 | return target * (1 / targetVectorMagnitude); 1168 | }); 1169 | } else this.force = { x: 0, y: 0 }; 1170 | 1171 | Enemy.prototype.step.call(this); 1172 | }; 1173 | 1174 | Eye.prototype.getRelativeDistance = function () { 1175 | var targetVector = this.getTargetVector(); 1176 | return vectorMagnitude(targetVector) / maximumPossibleDistanceBetweenTwoMasses; 1177 | }; 1178 | 1179 | Eye.prototype.getCalmness = function () { 1180 | return 1 / Math.pow(1 / this.getRelativeDistance(), 1 / 4); 1181 | }; 1182 | 1183 | Eye.prototype.drawWarning = function () { 1184 | var timeUntilSpawn = 1185 | (this.spawnAt - game.timeElapsed) / this.wave.spawnWarningDuration; 1186 | 1187 | draw({ 1188 | type: 'arc', 1189 | stroke: true, 1190 | lineWidth: ((2 * this.shadowRadius) / 2) * Math.pow(1 - timeUntilSpawn, 3), 1191 | strokeStyle: rgbWithOpacity( 1192 | this.rgbWarning || this.rgb, 1193 | (1 - timeUntilSpawn) * this.getOpacity() * this.shadowOpacity, 1194 | ), 1195 | arcCenter: this.position, 1196 | arcRadius: this.shadowRadius / 2 + Math.pow(timeUntilSpawn, 2) * 700, 1197 | }); 1198 | }; 1199 | 1200 | Eye.prototype.getIrisColor = function () { 1201 | var red = 0; 1202 | if (Math.random() < Math.pow(1 - this.getCalmness(), 4) * game.timeDelta) 1203 | red = 255; 1204 | return rgbWithOpacity([red, 0, 0], this.getOpacity()); 1205 | }; 1206 | 1207 | Eye.prototype.awakeness = function () { 1208 | var timeAlive = game.timeElapsed - this.spawnAt; 1209 | return 1 - 1 / (timeAlive / 3 + 1); 1210 | }; 1211 | 1212 | Eye.prototype.drawIris = function () { 1213 | var awakeness = this.awakeness(); 1214 | var targetVector = this.getTargetVector(); 1215 | var relativeDistance = this.getRelativeDistance(); 1216 | 1217 | var irisVector = vectorAt( 1218 | vectorAngle(targetVector), 1219 | awakeness * this.radius * Math.pow(relativeDistance, 1 / 2) * 0.7, 1220 | ); 1221 | 1222 | var centreOfIris = forXAndY([this.position, irisVector], forXAndY.add); 1223 | 1224 | var irisRadius = ((this.radius * 1) / 3) * awakeness; 1225 | 1226 | draw({ 1227 | type: 'arc', 1228 | fill: true, 1229 | fillStyle: this.getIrisColor(), 1230 | arcCenter: centreOfIris, 1231 | arcRadius: irisRadius, 1232 | }); 1233 | }; 1234 | 1235 | Eye.prototype.draw = function () { 1236 | draw({ 1237 | type: 'arc', 1238 | fill: true, 1239 | fillStyle: rgbWithOpacity([0, 0, 0], this.getOpacity() * this.shadowOpacity), 1240 | arcCenter: this.position, 1241 | arcRadius: this.shadowRadius, 1242 | }); 1243 | 1244 | this.visibleRadius = this.radius * Math.pow(this.awakeness(), 1 / 6); 1245 | Enemy.prototype.draw.call(this); 1246 | 1247 | if (this.died) return; 1248 | 1249 | this.drawIris(); 1250 | }; 1251 | 1252 | function Twitchy(opts) { 1253 | Enemy.call(this, opts); 1254 | this.charging = false; 1255 | 1256 | this.mass = 100; 1257 | this.lubricant = 0.92; 1258 | this.chargeRate = 0.01; 1259 | this.dischargeRate = 0.1; 1260 | this.radius = 5; 1261 | 1262 | this.fuel = 0.9; 1263 | this.rgbDischarging = [200, 30, 30]; 1264 | this.rgbWarning = this.rgbDischarging; 1265 | } 1266 | extend(Enemy, Twitchy); 1267 | 1268 | Twitchy.prototype.step = function () { 1269 | if (this.died || this.charging) { 1270 | this.force = { x: 0, y: 0 }; 1271 | if (this.charging) { 1272 | this.fuel += game.timeDelta * this.chargeRate; 1273 | if (this.fuel >= 1) { 1274 | this.fuel = 1; 1275 | this.charging = false; 1276 | } 1277 | } 1278 | } else { 1279 | this.force = this.getTargetVector(); 1280 | this.fuel -= game.timeDelta * this.dischargeRate; 1281 | 1282 | if (this.fuel <= 0) { 1283 | this.fuel = 0; 1284 | this.charging = true; 1285 | } 1286 | } 1287 | 1288 | Enemy.prototype.step.call(this); 1289 | }; 1290 | 1291 | Twitchy.prototype.getCurrentColor = function () { 1292 | if (this.charging) { 1293 | var brightness = 255; 1294 | var whiteness = Math.pow(this.fuel, 1 / 40); 1295 | 1296 | if (0.98 < this.fuel || (0.94 < this.fuel && this.fuel < 0.96)) { 1297 | brightness = 0; 1298 | } 1299 | 1300 | this.rgb = [brightness, brightness * whiteness, brightness * whiteness]; 1301 | } else this.rgb = this.rgbDischarging; 1302 | 1303 | return Enemy.prototype.getCurrentColor.call(this); 1304 | }; 1305 | 1306 | Twitchy.prototype.draw = function () { 1307 | if (this.charging && this.fuel >= 0) { 1308 | draw({ 1309 | type: 'arc', 1310 | fill: true, 1311 | fillStyle: rgbWithOpacity([30, 30, 30], this.getOpacity() * this.fuel), 1312 | arcRadius: (this.radius * 1.2) / this.fuel, 1313 | arcCenter: this.position, 1314 | }); 1315 | } 1316 | 1317 | Enemy.prototype.draw.call(this); 1318 | }; 1319 | 1320 | function Particle() { 1321 | Mass.call(this); 1322 | game.particles.push(this); 1323 | } 1324 | extend(Mass, Particle); 1325 | Particle.prototype.isWorthDestroying = function () { 1326 | return Math.abs(this.velocity.x) < 0.001 && Math.abs(this.velocity.y) < 0.001; 1327 | }; 1328 | 1329 | function FireParticle(position, velocity) { 1330 | Particle.call(this); 1331 | this.lubricant = 0.9; 1332 | this.created = game.timeElapsed; 1333 | this.teleportTo(position); 1334 | this.velocity = velocity; 1335 | this.red = 1; 1336 | this.green = 1; 1337 | this.blue = 0; 1338 | this.opacity = 1; 1339 | 1340 | this.initialIntensity = velocity.x * (2 * Math.random()); 1341 | } 1342 | extend(Particle, FireParticle); 1343 | 1344 | FireParticle.prototype.getCurrentColor = function () { 1345 | var intensity = this.velocity.x / this.initialIntensity; 1346 | return rgbWithOpacity( 1347 | this.rgbForIntensity(intensity), 1348 | Math.pow(intensity, 0.25) * this.opacity, 1349 | ); 1350 | }; 1351 | 1352 | FireParticle.prototype.rgbForIntensity = function (intensity) { 1353 | return [Math.pow(intensity, 0.2) * 255, intensity * 200, 0]; 1354 | }; 1355 | 1356 | FireParticle.prototype.draw = function () { 1357 | if (Math.random() < 0.1 * game.timeDelta) return; 1358 | 1359 | var timeAlive = game.timeElapsed - this.created; 1360 | var maturity = 1 - 1 / (timeAlive / 3 + 1); 1361 | var velocityButSmallerWhenYoung = forXAndY( 1362 | [this.velocity, { x: maturity, y: maturity }], 1363 | forXAndY.multiply, 1364 | ); 1365 | 1366 | draw({ 1367 | type: 'line', 1368 | stroke: true, 1369 | strokeStyle: this.getCurrentColor(), 1370 | linePaths: [ 1371 | [ 1372 | this.position, 1373 | forXAndY([this.position, velocityButSmallerWhenYoung], forXAndY.aPlusHalfB), 1374 | ], 1375 | ], 1376 | }); 1377 | }; 1378 | 1379 | function Exhaust(source) { 1380 | var position = source.position; 1381 | 1382 | var delta = source.velocityDelta(); 1383 | var baseVelocity = forXAndY([source.velocity, delta], function (v, d) { 1384 | return 0.3 * v - d * 20; 1385 | }); 1386 | 1387 | var deltaMagnitude = vectorMagnitude(delta); 1388 | var velocity = forXAndY([baseVelocity], function (b) { 1389 | return b * (1 + (Math.random() - 0.5) * (0.8 + deltaMagnitude * 0.1)); 1390 | }); 1391 | 1392 | FireParticle.call(this, position, velocity); 1393 | 1394 | this.opacity = 0.7; 1395 | } 1396 | extend(FireParticle, Exhaust); 1397 | 1398 | Exhaust.prototype.rgbForIntensity = function (intensity) { 1399 | return [intensity * 200, 50 + intensity * 100, 50 + intensity * 100]; 1400 | }; 1401 | 1402 | function TeleportDust(source) { 1403 | var randomDelta = vectorAt( 1404 | Math.random() * Math.PI * 2, 1405 | Math.random() * source.radius * 0.1, 1406 | ); 1407 | 1408 | var velocityMultiplier = (Math.random() * 1) / 10; 1409 | var baseVelocity = forXAndY( 1410 | [source.teleportDelta, { x: velocityMultiplier, y: velocityMultiplier }], 1411 | forXAndY.multiply, 1412 | ); 1413 | var velocity = forXAndY([baseVelocity, randomDelta], forXAndY.add); 1414 | 1415 | var distanceFromStart = Math.random(); 1416 | var vectorFromStart = forXAndY( 1417 | [source.teleportDelta, { x: distanceFromStart, y: distanceFromStart }], 1418 | forXAndY.multiply, 1419 | ); 1420 | var basePosition = forXAndY([source.position, vectorFromStart], forXAndY.add); 1421 | var position = forXAndY([basePosition, randomDelta], forXAndY.add); 1422 | 1423 | FireParticle.call(this, position, velocity); 1424 | } 1425 | extend(FireParticle, TeleportDust); 1426 | 1427 | TeleportDust.prototype.rgbForIntensity = function (intensity) { 1428 | return [100 + intensity * 100, intensity * 200, 60 + intensity * 150]; 1429 | }; 1430 | 1431 | function Wave() { 1432 | this.enemies = []; 1433 | this.complete = false; 1434 | this.doneSpawningEnemies = false; 1435 | this.spawnWarningDuration = 50; 1436 | this.boredomCompensation = 0; 1437 | this.startedAt = game.timeElapsed; 1438 | } 1439 | 1440 | Wave.prototype.step = function () { 1441 | this.spawnEnemies(); 1442 | 1443 | this.remainingLivingEnemies = 0; 1444 | 1445 | for (var i = 0; i < this.enemies.length; i++) { 1446 | var enemy = this.enemies[i]; 1447 | if (enemy.spawned) enemy.step(); 1448 | else if (enemy.spawnAt <= game.timeElapsed) enemy.spawned = true; 1449 | 1450 | if (!enemy.died) this.remainingLivingEnemies++; 1451 | } 1452 | 1453 | if (this.remainingLivingEnemies >= 15) unlockAchievement('panic'); 1454 | if ( 1455 | this.doneSpawningEnemies && 1456 | this.remainingLivingEnemies === 0 && 1457 | !this.hasEnemiesWorthDrawing 1458 | ) 1459 | this.complete = true; 1460 | }; 1461 | 1462 | Wave.prototype.draw = function () { 1463 | this.hasEnemiesWorthDrawing = false; 1464 | 1465 | for (var i = 0; i < this.enemies.length; i++) { 1466 | var enemy = this.enemies[i]; 1467 | var opacity = enemy.getOpacity(); 1468 | if (opacity > 0.01) { 1469 | if (enemy.spawned) enemy.draw(); 1470 | else enemy.drawWarning(); 1471 | 1472 | this.hasEnemiesWorthDrawing = true; 1473 | } 1474 | } 1475 | }; 1476 | 1477 | Wave.prototype.spawnEnemies = function () { 1478 | if (this.doneSpawningEnemies) return; 1479 | 1480 | var remaininUnspawnedEnemies = 0; 1481 | var totalDelay = this.boredomCompensation; 1482 | var compensatedForBoredom = false; 1483 | 1484 | for (var i = 0; i < this.spawns.length; i++) { 1485 | var spawn = this.spawns[i]; 1486 | 1487 | totalDelay += spawn.delay; 1488 | 1489 | if (spawn.spawned) continue; 1490 | 1491 | var timeUntilSpawn = totalDelay - (game.timeElapsed - this.startedAt); 1492 | 1493 | if (!compensatedForBoredom && this.remainingLivingEnemies === 0) { 1494 | compensatedForBoredom = true; 1495 | this.boredomCompensation += timeUntilSpawn; 1496 | timeUntilSpawn -= this.boredomCompensation; 1497 | } 1498 | 1499 | if (timeUntilSpawn <= 0) { 1500 | var opts = spawn.opts || {}; 1501 | 1502 | opts.spawnAt = game.timeElapsed + this.spawnWarningDuration; 1503 | opts.wave = this; 1504 | 1505 | var enemy = new spawn.type(opts); 1506 | 1507 | if (spawn.pos) { 1508 | enemy.teleportTo({ 1509 | x: spawn.pos[0] * width, 1510 | y: spawn.pos[1] * height, 1511 | }); 1512 | } else enemy.teleportTo(enemy.randomSpawnPosition()); 1513 | 1514 | this.enemies.push(enemy); 1515 | 1516 | spawn.spawned = true; 1517 | } else { 1518 | remaininUnspawnedEnemies++; 1519 | } 1520 | } 1521 | 1522 | if (remaininUnspawnedEnemies === 0) this.doneSpawningEnemies = true; 1523 | }; 1524 | 1525 | function tutorialFor(enemyType, enemyOpts) { 1526 | function Tutorial() { 1527 | Wave.call(this); 1528 | this.spawns = [ 1529 | { 1530 | delay: 0, 1531 | type: enemyType, 1532 | pos: [1 / 2, 1 / 5], 1533 | opts: enemyOpts || {}, 1534 | }, 1535 | ]; 1536 | } 1537 | extend(Wave, Tutorial); 1538 | return Tutorial; 1539 | } 1540 | 1541 | function aBunchOf(enemyType, count, interval) { 1542 | function ABunch() { 1543 | Wave.call(this); 1544 | this.spawns = []; 1545 | 1546 | for (var i = 0; i < count; i++) { 1547 | this.spawns.push({ 1548 | delay: interval * (i + 1), 1549 | type: enemyType, 1550 | }); 1551 | } 1552 | } 1553 | extend(Wave, ABunch); 1554 | return ABunch; 1555 | } 1556 | 1557 | function autoWave(difficulty) { 1558 | var totalSpawns; 1559 | var localEnemyPool; 1560 | 1561 | if (difficulty % 2) { 1562 | totalSpawns = 15 + difficulty; 1563 | localEnemyPool = enemyPool; 1564 | } else { 1565 | localEnemyPool = [enemyPool[(difficulty / 2) % enemyPool.length]]; 1566 | totalSpawns = 10 + difficulty; 1567 | } 1568 | 1569 | function AutoWave() { 1570 | Wave.call(this); 1571 | this.spawns = []; 1572 | 1573 | for (var i = 0; i < totalSpawns; i++) { 1574 | this.spawns.push({ 1575 | delay: (Math.pow(Math.random(), 1 / 2) * 400) / (difficulty + 7), 1576 | type: choice(localEnemyPool), 1577 | }); 1578 | } 1579 | } 1580 | 1581 | extend(Wave, AutoWave); 1582 | return AutoWave; 1583 | } 1584 | 1585 | function saveCookie(key, value) { 1586 | storage.setItem(key, value); 1587 | document.cookie = key + '=' + value + cookieSuffix; 1588 | } 1589 | 1590 | function unlockAchievement(slug) { 1591 | var achievement = achievements[slug]; 1592 | if (!achievement.unlocked) { 1593 | achievement.unlocked = new Date(); 1594 | saveCookie(slug, achievement.unlocked.getTime().toString()); 1595 | } 1596 | } 1597 | 1598 | function logScore(score) { 1599 | if (score > highScore) { 1600 | highScore = score; 1601 | saveCookie(highScoreCookieKey, score.toString()); 1602 | } 1603 | } 1604 | 1605 | function getUnlockedAchievements(invert) { 1606 | var unlockedAchievements = []; 1607 | invert = invert || false; 1608 | 1609 | for (var key in achievements) { 1610 | var achievement = achievements[key]; 1611 | if (invert ^ (achievement.unlocked !== undefined)) 1612 | unlockedAchievements.push(achievement); 1613 | } 1614 | 1615 | return unlockedAchievements; 1616 | } 1617 | 1618 | function getLockedAchievements() { 1619 | return getUnlockedAchievements(true); 1620 | } 1621 | 1622 | function Game() { 1623 | var self = this; 1624 | 1625 | self.lastMousePosition = { x: NaN, y: NaN }; 1626 | 1627 | self.reset = function (waveIndex) { 1628 | if (document.pointerLockElement) document.exitPointerLock(); 1629 | 1630 | self.background = new Background(); 1631 | self.ended = null; 1632 | self.score = 0; 1633 | self.enemyTypesKilled = []; 1634 | self.lastPointScoredAt = 0; 1635 | self.timeElapsed = 0; 1636 | self.normalSpeed = 0.04; 1637 | self.slowSpeed = self.normalSpeed / 100; 1638 | self.setSpeed(self.normalSpeed); 1639 | 1640 | self.started = false; 1641 | 1642 | self.waveIndex = waveIndex || 0; 1643 | self.waves = [ 1644 | tutorialFor(Drifter), 1645 | aBunchOf(Drifter, 2, 5), 1646 | 1647 | tutorialFor(Eye, { size: 1.5 }), 1648 | aBunchOf(Eye, 4, 100), 1649 | aBunchOf(Eye, 5, 10), 1650 | 1651 | tutorialFor(Twitchy), 1652 | aBunchOf(Twitchy, 4, 50), 1653 | aBunchOf(Twitchy, 5, 10), 1654 | ]; 1655 | self.wave = undefined; 1656 | 1657 | self.particles = []; 1658 | 1659 | self.tether = new Tether(); 1660 | self.player = new Player(self.tether); 1661 | self.cable = new Cable(self.tether, self.player); 1662 | }; 1663 | 1664 | self.setSpeed = function (speed) { 1665 | self.speed = speed; 1666 | }; 1667 | 1668 | self.start = function () { 1669 | self.tether.locked = false; 1670 | self.player.lubricant = self.player.onceGameHasStartedLubricant; 1671 | self.started = true; 1672 | self.timeElapsed = 0; 1673 | }; 1674 | 1675 | self.pickNextWave = function () { 1676 | var waveType = self.waves[self.waveIndex++]; 1677 | 1678 | if (waveType === undefined) { 1679 | waveType = autoWave(self.waveIndex - self.waves.length); 1680 | } 1681 | 1682 | self.wave = new waveType(); 1683 | }; 1684 | 1685 | self.incrementScore = function (incr) { 1686 | self.lastPointScoredAt = self.timeElapsed; 1687 | self.score += incr; 1688 | self.tether.pointsScoredSinceLastInteraction += incr; 1689 | 1690 | if (self.score >= 10 && width <= 500 && height <= 500) { 1691 | unlockAchievement('lowRes'); 1692 | } 1693 | 1694 | if (self.tether.pointsScoredSinceLastInteraction >= 5) { 1695 | unlockAchievement('handsFree'); 1696 | } 1697 | }; 1698 | 1699 | self.getIntensity = function () { 1700 | return 1 / (1 + (self.timeElapsed - self.lastPointScoredAt)); 1701 | }; 1702 | 1703 | self.stepParticles = function () { 1704 | for (var i = 0; i < self.particles.length; i++) { 1705 | if (self.particles[i] === undefined) { 1706 | continue; 1707 | } else if (self.particles[i].isWorthDestroying()) { 1708 | delete self.particles[i]; 1709 | } else { 1710 | self.particles[i].step(); 1711 | } 1712 | } 1713 | }; 1714 | 1715 | self.step = function () { 1716 | if (DEBUG) draw({ type: 'clear' }); 1717 | 1718 | var now = new Date().getTime(); 1719 | 1720 | if (!self.lastStepped) { 1721 | self.lastStepped = now; 1722 | return; 1723 | } else { 1724 | self.realTimeDelta = now - self.lastStepped; 1725 | 1726 | self.timeDelta = Math.min(self.realTimeDelta, 1000 / 20) * self.speed; 1727 | 1728 | self.timeElapsed += self.timeDelta; 1729 | self.lastStepped = now; 1730 | } 1731 | 1732 | if (isNaN(self.lastMousePosition.x)) { 1733 | self.proximityToMuteButton = maximumPossibleDistanceBetweenTwoMasses; 1734 | self.proximityToPlayButton = maximumPossibleDistanceBetweenTwoMasses; 1735 | } else { 1736 | self.proximityToMuteButton = vectorMagnitude( 1737 | forXAndY([muteButtonPosition, self.lastMousePosition], forXAndY.subtract), 1738 | ); 1739 | self.proximityToPlayButton = vectorMagnitude( 1740 | forXAndY([playButtonPosition, self.lastMousePosition], forXAndY.subtract), 1741 | ); 1742 | } 1743 | self.clickShouldMute = 1744 | (!self.started || self.ended) && 1745 | self.proximityToMuteButton < muteButtonProximityThreshold 1746 | ? true 1747 | : false; 1748 | self.clickShouldPlay = 1749 | self.started && 1750 | !self.ended && 1751 | self.proximityToPlayButton < playButtonProximityThreshold 1752 | ? true 1753 | : false; 1754 | if (self.clickShouldMute !== canvas.classList.contains('buttonhover')) 1755 | canvas.classList.toggle('buttonhover'); 1756 | if (self.clickShouldPlay !== canvas.classList.contains('buttonhover')) 1757 | canvas.classList.toggle('buttonhover'); 1758 | 1759 | self.background.step(); 1760 | self.tether.step(); 1761 | self.player.step(); 1762 | 1763 | if (self.started) { 1764 | if (self.wave === undefined || self.wave.complete) self.pickNextWave(); 1765 | self.wave.step(); 1766 | 1767 | if (!self.ended) self.checkForEnemyContact(); 1768 | self.checkForCableContact(); 1769 | } 1770 | 1771 | self.stepParticles(); 1772 | 1773 | self.draw(); 1774 | }; 1775 | 1776 | self.checkForCableContact = function () { 1777 | var cableAreaCovered = self.cable.areaCoveredThisStep(); 1778 | 1779 | for (var i = 0; i < self.wave.enemies.length; i++) { 1780 | var enemy = self.wave.enemies[i]; 1781 | 1782 | if (enemy.died || !enemy.spawned) { 1783 | continue; 1784 | } 1785 | 1786 | var journey = enemy.journeySincePreviousFrame(); 1787 | var cableLines = linesFromPolygon(cableAreaCovered); 1788 | 1789 | if (pointInPolygon(enemy.position, cableAreaCovered)) { 1790 | enemy.die(true); 1791 | continue; 1792 | } 1793 | 1794 | for (var ci = 0; ci < cableLines.length; ci++) { 1795 | var intersection = getIntersection(journey, cableLines[ci]); 1796 | 1797 | if (intersection.onLine1 && intersection.onLine2) { 1798 | enemy.position = intersection; 1799 | enemy.die(true); 1800 | break; 1801 | } 1802 | } 1803 | } 1804 | }; 1805 | 1806 | self.checkForEnemyContactWith = function (mass) { 1807 | var massPositionDelta = lineDelta([ 1808 | mass.positionOnPreviousFrame, 1809 | mass.position, 1810 | ]); 1811 | 1812 | var colChecks = []; 1813 | 1814 | for (var i = 0; i < self.wave.enemies.length; i++) { 1815 | var enemy = self.wave.enemies[i]; 1816 | 1817 | if (enemy.died || !enemy.spawned) { 1818 | continue; 1819 | } 1820 | 1821 | var enemyPositionDelta = lineDelta([ 1822 | enemy.positionOnPreviousFrame, 1823 | enemy.position, 1824 | ]); 1825 | 1826 | for ( 1827 | var progress = 0; 1828 | progress < 1; 1829 | progress += 1830 | Math.min(enemy.radius, mass.radius) / 1831 | (3 * 1832 | Math.max( 1833 | enemyPositionDelta.x, 1834 | enemyPositionDelta.y, 1835 | massPositionDelta.x, 1836 | massPositionDelta.y, 1837 | 1, 1838 | )) 1839 | ) { 1840 | enemyPosition = { 1841 | x: enemy.positionOnPreviousFrame.x + enemyPositionDelta.x * progress, 1842 | y: enemy.positionOnPreviousFrame.y + enemyPositionDelta.y * progress, 1843 | }; 1844 | 1845 | massPosition = { 1846 | x: mass.positionOnPreviousFrame.x + massPositionDelta.x * progress, 1847 | y: mass.positionOnPreviousFrame.y + massPositionDelta.y * progress, 1848 | }; 1849 | 1850 | if (INFO) this.collisionChecks += 1; 1851 | if (DEBUG) colChecks.push([enemyPosition, massPosition]); 1852 | 1853 | var distance = lineDelta([enemyPosition, massPosition]); 1854 | 1855 | if ( 1856 | Math.pow(distance.x, 2) + Math.pow(distance.y, 2) < 1857 | Math.pow(enemy.radius + mass.radius, 2) 1858 | ) { 1859 | enemy.position = enemyPosition; 1860 | mass.position = massPosition; 1861 | enemy.die(false); 1862 | 1863 | if (mass === this.player) { 1864 | var relativeVelocity = lineDelta([mass.velocity, enemy.velocity]); 1865 | var impact = 1866 | vectorMagnitude(relativeVelocity) / 1867 | maximumPossibleDistanceBetweenTwoMasses; 1868 | 1869 | if (impact > 0.04) unlockAchievement('impact'); 1870 | if (INFO) console.log('impact: ' + impact.toString()); 1871 | } 1872 | 1873 | return mass; 1874 | } 1875 | } 1876 | } 1877 | 1878 | if (DEBUG) 1879 | draw({ 1880 | type: 'line', 1881 | stroke: true, 1882 | linePaths: colChecks, 1883 | strokeStyle: rgbWithOpacity([0, 127, 0], 0.3), 1884 | }); 1885 | }; 1886 | 1887 | self.checkForEnemyContact = function () { 1888 | if (INFO) this.collisionChecks = 0; 1889 | var deadMass = 1890 | self.checkForEnemyContactWith(self.tether) || 1891 | self.checkForEnemyContactWith(self.player); 1892 | if (deadMass) { 1893 | deadMass.rgb = [200, 20, 20]; 1894 | deadMass.explode(); 1895 | unlockAchievement('die'); 1896 | if (game.score === 1) unlockAchievement('introduction'); 1897 | game.end(); 1898 | } 1899 | }; 1900 | 1901 | self.drawScore = function () { 1902 | if (self.score === 0) return; 1903 | 1904 | var intensity = self.getIntensity(); 1905 | 1906 | draw({ 1907 | type: 'text', 1908 | text: self.score.toString(), 1909 | fontSize: intensity * height * 5, 1910 | fillStyle: rgbWithOpacity([0, 0, 0], intensity), 1911 | textPosition: { x: width / 2, y: height / 2 }, 1912 | }); 1913 | }; 1914 | 1915 | self.drawParticles = function () { 1916 | for (var i = 0; i < this.particles.length; i++) { 1917 | if (this.particles[i] !== undefined) { 1918 | this.particles[i].draw(); 1919 | } 1920 | } 1921 | }; 1922 | 1923 | self.drawLogo = function () { 1924 | var opacity = game.started ? Math.pow(1 - game.timeElapsed / 50, 3) : 1; 1925 | if (opacity < 0.001) return; 1926 | 1927 | draw({ 1928 | type: 'text', 1929 | text: 'tether!', 1930 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 1931 | fontSize: 100, 1932 | textPosition: { 1933 | x: width / 2, 1934 | y: height / 3, 1935 | }, 1936 | }); 1937 | 1938 | draw({ 1939 | type: 'text', 1940 | text: subtitleText ?? 'Swing around a ball and cause pure destruction.', 1941 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 1942 | fontSize: 30, 1943 | textPosition: { 1944 | x: width / 2, 1945 | y: height / 3 + 55, 1946 | }, 1947 | }); 1948 | 1949 | draw({ 1950 | type: 'text', 1951 | text: 1952 | ({ touch: 'tap', mouse: 'click' }[self.tether.lastInteraction] ?? 'click') + 1953 | ' to start', 1954 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 1955 | fontSize: 24, 1956 | textPosition: { 1957 | x: width / 2, 1958 | y: (height / 4) * 3 + 80, 1959 | }, 1960 | }); 1961 | }; 1962 | 1963 | self.drawRestartTutorial = function () { 1964 | if (!self.ended) return; 1965 | 1966 | var opacity = -Math.sin((game.timeElapsed - game.ended) * 3); 1967 | if (opacity < 0) opacity = 0; 1968 | 1969 | var fontSize = Math.min(width / 5, height / 8); 1970 | 1971 | draw({ 1972 | type: 'text', 1973 | text: 1974 | ({ touch: 'tap', mouse: 'click' }[self.tether.lastInteraction] ?? 'click') + 1975 | ' to retry', 1976 | fontSize: fontSize, 1977 | textPosition: { x: width / 2, y: height / 2 - fontSize / 2 }, 1978 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 1979 | }); 1980 | }; 1981 | 1982 | self.drawAchievementNotifications = function () { 1983 | var now = new Date().getTime(); 1984 | var recentAchievements = []; 1985 | var animationDuration = 7000; 1986 | 1987 | for (var slug in achievements) { 1988 | var achievement = achievements[slug]; 1989 | if (achievement.unlocked === undefined) continue; 1990 | 1991 | var unlocked = achievement.unlocked.getTime(); 1992 | 1993 | if (now > unlocked && now < unlocked + animationDuration) { 1994 | recentAchievements.push(achievement); 1995 | } 1996 | } 1997 | 1998 | for (var i = 0; i < recentAchievements.length; i++) { 1999 | var recentAchievement = recentAchievements[i]; 2000 | var progress = (now - recentAchievement.unlocked) / animationDuration; 2001 | 2002 | var visibility = 1; 2003 | var buffer = 0.2; 2004 | 2005 | var easing = 6; 2006 | 2007 | if (progress < buffer) visibility = Math.pow(progress / buffer, 1 / easing); 2008 | else if (progress > 1 - buffer) 2009 | visibility = Math.pow((1 - progress) / buffer, easing); 2010 | 2011 | var sink = -50 * (1 - visibility); 2012 | var notificationHeight = 60; 2013 | var baseNotificationHeight = 20 + notificationHeight * i; 2014 | 2015 | var drawArgs = { 2016 | type: 'text', 2017 | text: 'Achievement Unlocked', 2018 | textAlign: 'right', 2019 | textBaseline: 'top', 2020 | fillStyle: rgbWithOpacity([0, 0, 0], visibility), 2021 | fontFamily: 'Quantico', 2022 | fontSize: 17, 2023 | textPosition: { 2024 | x: width - 25, 2025 | y: visibility * baseNotificationHeight + sink, 2026 | }, 2027 | }; 2028 | 2029 | draw(drawArgs); 2030 | 2031 | drawArgs.fontSize = 25; 2032 | drawArgs.text = recentAchievement.name; 2033 | drawArgs.textPosition = { 2034 | x: width - 25, 2035 | y: 19 + visibility * baseNotificationHeight + sink, 2036 | }; 2037 | 2038 | draw(drawArgs); 2039 | } 2040 | }; 2041 | 2042 | self.drawAchievements = function ( 2043 | achievementList, 2044 | fromBottom, 2045 | fromRight, 2046 | headingText, 2047 | fillStyle, 2048 | ) { 2049 | if (achievementList.length === 0) return fromBottom; 2050 | 2051 | var drawOpts = { 2052 | type: 'text', 2053 | fillStyle: fillStyle, 2054 | textAlign: 'right', 2055 | fontFamily: 'Quantico', 2056 | textBaseline: 'alphabetic', 2057 | }; 2058 | var xPos = width - fromRight; 2059 | 2060 | for (var i = 0; i < achievementList.length; i++) { 2061 | var achievement = achievementList[i]; 2062 | 2063 | drawOpts.text = achievement.name; 2064 | drawOpts.fontSize = 18; 2065 | drawOpts.textPosition = { x: xPos, y: height - fromBottom - 16 }; 2066 | draw(drawOpts); 2067 | 2068 | drawOpts.text = achievement.description; 2069 | drawOpts.fontSize = 13; 2070 | drawOpts.textPosition = { x: xPos, y: height - fromBottom }; 2071 | draw(drawOpts); 2072 | 2073 | fromBottom += 45; 2074 | } 2075 | 2076 | drawOpts.text = headingText; 2077 | drawOpts.fontSize = 20; 2078 | drawOpts.textPosition = { x: xPos, y: height - fromBottom }; 2079 | draw(drawOpts); 2080 | 2081 | fromBottom += 55; 2082 | return fromBottom; 2083 | }; 2084 | 2085 | self.drawPauseMessage = function () { 2086 | var fontSize = Math.min(width / 5, height / 8); 2087 | draw({ 2088 | type: 'text', 2089 | text: 2090 | ({ touch: 'tap', mouse: 'click' }[self.tether.lastInteraction] ?? 'click') + 2091 | ' to unpause', 2092 | fillStyle: '#000', 2093 | fontSize: fontSize, 2094 | textPosition: { 2095 | x: width / 2, 2096 | y: height / 2 - fontSize / 2, 2097 | }, 2098 | }); 2099 | }; 2100 | 2101 | self.drawAchievementUI = function () { 2102 | var unlockedAchievements = getUnlockedAchievements(); 2103 | if (unlockedAchievements.length > 0) { 2104 | var indicatedPosition = { x: 0, y: 0 }; 2105 | if (isNaN(game.lastMousePosition.x)) { 2106 | indicatedPosition = { x: 0, y: 0 }; 2107 | } else { 2108 | indicatedPosition = game.lastMousePosition; 2109 | } 2110 | var distanceFromCorner = vectorMagnitude( 2111 | lineDelta([indicatedPosition, { x: width, y: height }]), 2112 | ); 2113 | var distanceRange = [ 2114 | maximumPossibleDistanceBetweenTwoMasses / 10, 2115 | maximumPossibleDistanceBetweenTwoMasses / 4, 2116 | ]; 2117 | var hintOpacity; 2118 | 2119 | if (distanceFromCorner > distanceRange[1]) hintOpacity = 1; 2120 | else if (distanceFromCorner > distanceRange[0]) 2121 | hintOpacity = 2122 | (distanceFromCorner - distanceRange[0]) / 2123 | (distanceRange[1] - distanceRange[0]); 2124 | else hintOpacity = 0; 2125 | 2126 | var listingOpacity = 1 - hintOpacity; 2127 | 2128 | draw({ 2129 | type: 'text', 2130 | text: 'Achievements…', 2131 | fillStyle: (fillStyle = rgbWithOpacity([0, 0, 0], hintOpacity)), 2132 | fontSize: 16, 2133 | textPosition: { x: width - 5, y: height - 8 }, 2134 | textAlign: 'right', 2135 | textBaseline: 'alphabetic', 2136 | fontFamily: 'Quantico', 2137 | }); 2138 | 2139 | if (highScore) { 2140 | draw({ 2141 | type: 'text', 2142 | text: 'Best Score: ' + highScore.toString(), 2143 | fillStyle: (fillStyle = rgbWithOpacity([0, 0, 0], hintOpacity)), 2144 | fontSize: 16, 2145 | textPosition: { x: width - 6, y: height - 56 }, 2146 | textAlign: 'right', 2147 | textBaseline: 'bottom', 2148 | fontFamily: 'Quantico', 2149 | }); 2150 | } 2151 | 2152 | draw({ 2153 | type: 'text', 2154 | text: 'Login Streak: ' + streakCount.toString(), 2155 | fillStyle: (fillStyle = rgbWithOpacity([0, 0, 0], hintOpacity)), 2156 | fontSize: 16, 2157 | textPosition: { x: width - 6, y: height - 38 }, 2158 | textAlign: 'right', 2159 | textBaseline: 'bottom', 2160 | fontFamily: 'Quantico', 2161 | }); 2162 | 2163 | draw({ 2164 | type: 'text', 2165 | text: 'Next Day: ' + timeToNextClaim(), 2166 | fillStyle: (fillStyle = rgbWithOpacity([0, 0, 0], hintOpacity)), 2167 | fontSize: 16, 2168 | textPosition: { x: width - 6, y: height - 20 }, 2169 | textAlign: 'right', 2170 | textBaseline: 'bottom', 2171 | fontFamily: 'Quantico', 2172 | }); 2173 | 2174 | draw({ 2175 | type: 'rect', 2176 | rectBounds: [0, 0, width, height], 2177 | fillStyle: rgbWithOpacity([255, 255, 255], listingOpacity * 0.9), 2178 | }); 2179 | 2180 | var heightNeeded = 500; 2181 | var widthNeeded = 500; 2182 | var fromBottom = 2183 | ((game.lastMousePosition.y - height) / height) * heightNeeded + 40; 2184 | var fromRight = 2185 | ((game.lastMousePosition.x - width) / width) * widthNeeded + 35; 2186 | fromBottom = this.drawAchievements( 2187 | getLockedAchievements(), 2188 | fromBottom, 2189 | fromRight, 2190 | 'Locked', 2191 | rgbWithOpacity([0, 0, 0], listingOpacity * 0.5), 2192 | ); 2193 | this.drawAchievements( 2194 | unlockedAchievements, 2195 | fromBottom, 2196 | fromRight, 2197 | 'Unlocked', 2198 | rgbWithOpacity([0, 0, 0], listingOpacity), 2199 | ); 2200 | } 2201 | }; 2202 | 2203 | self.eventShouldMute = function (e) { 2204 | var position; 2205 | 2206 | if (e.changedTouches) { 2207 | var touch = e.changedTouches[0]; 2208 | position = { x: touch.pageX, y: touch.pageY }; 2209 | } else { 2210 | position = { x: e.layerX, y: e.layerY }; 2211 | } 2212 | 2213 | return self.positionShouldMute(position); 2214 | }; 2215 | 2216 | self.positionShouldMute = function (position) { 2217 | if (self.started || self.ended) return false; 2218 | self.proximityToMuteButton = vectorMagnitude( 2219 | forXAndY([muteButtonPosition, position], forXAndY.subtract), 2220 | ); 2221 | return self.proximityToMuteButton < muteButtonProximityThreshold; 2222 | }; 2223 | 2224 | self.eventShouldPlay = function (e) { 2225 | var position; 2226 | 2227 | if (e.changedTouches) { 2228 | var touch = e.changedTouches[0]; 2229 | position = { x: touch.pageX, y: touch.pageY }; 2230 | } else { 2231 | position = game.lastMousePosition || { x: e.layerX, y: e.layerY }; 2232 | } 2233 | 2234 | return self.positionShouldPlay(position); 2235 | }; 2236 | 2237 | self.positionShouldPlay = function (position) { 2238 | if (!(self.started && !self.ended)) return false; 2239 | if (paused) return true; 2240 | self.proximityToPlayButton = vectorMagnitude( 2241 | forXAndY([playButtonPosition, position], forXAndY.subtract), 2242 | ); 2243 | return self.proximityToPlayButton < playButtonProximityThreshold; 2244 | }; 2245 | 2246 | self.drawMuteButton = function () { 2247 | if (!self.clickShouldMute && music.element.paused) { 2248 | xNoise = (Math.random() - 0.5) * (500 / self.proximityToMuteButton); 2249 | yNoise = (Math.random() - 0.5) * (500 / self.proximityToMuteButton); 2250 | visiblePosition = { 2251 | x: xNoise + muteButtonPosition.x, 2252 | y: yNoise + muteButtonPosition.y + Math.sin(new Date().getTime() / 250) * 3, 2253 | }; 2254 | } else { 2255 | visiblePosition = { x: muteButtonPosition.x, y: muteButtonPosition.y }; 2256 | } 2257 | 2258 | if (!music.element.paused) { 2259 | visiblePosition.x = visiblePosition.x - 5; 2260 | visiblePosition.y = visiblePosition.y - 2; 2261 | } 2262 | 2263 | var opacity = 1; 2264 | 2265 | if (self.clickShouldMute && !music.element.paused) opacity = 0.5; 2266 | 2267 | draw({ 2268 | type: 'text', 2269 | text: music.element.paused ? '\uf025' : '\uf026', 2270 | fontFamily: 'FontAwesome', 2271 | fontSize: 30, 2272 | textAlign: 'center', 2273 | textBaseline: 'middle', 2274 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 2275 | textPosition: visiblePosition, 2276 | }); 2277 | }; 2278 | 2279 | self.drawPlayButton = function () { 2280 | if (!self.clickShouldPlay && paused) { 2281 | xNoise = (Math.random() - 0.5) * (500 / self.proximityToPlayButton); 2282 | yNoise = (Math.random() - 0.5) * (500 / self.proximityToPlayButton); 2283 | visiblePosition = { 2284 | x: xNoise + playButtonPosition.x, 2285 | y: yNoise + playButtonPosition.y + Math.sin(new Date().getTime() / 250) * 3, 2286 | }; 2287 | } else { 2288 | visiblePosition = { x: playButtonPosition.x, y: playButtonPosition.y }; 2289 | } 2290 | 2291 | var opacity = 1; 2292 | 2293 | if (self.clickShouldPlay && !paused) opacity = 0.5; 2294 | 2295 | draw({ 2296 | type: 'text', 2297 | text: paused ? '\uf04b' : '\uf04c', 2298 | fontFamily: 'FontAwesome', 2299 | fontSize: 30, 2300 | textAlign: 'center', 2301 | textBaseline: 'middle', 2302 | fillStyle: rgbWithOpacity([0, 0, 0], opacity), 2303 | textPosition: visiblePosition, 2304 | }); 2305 | }; 2306 | 2307 | self.drawInfo = function () { 2308 | var fromBottom = 7; 2309 | var info = { 2310 | beat: Math.floor(music.beat()), 2311 | measure: Math.floor(music.measure()) + 1, 2312 | time: self.timeElapsed.toFixed(2), 2313 | fps: (1000 / self.realTimeDelta).toFixed(), 2314 | score: game.score, 2315 | }; 2316 | 2317 | if (self.started) { 2318 | info.wave = this.waveIndex.toString() + ' - ' + this.wave.constructor.name; 2319 | info.colchecks = self.collisionChecks.toFixed(); 2320 | } 2321 | 2322 | for (var key in info) { 2323 | draw({ 2324 | type: 'text', 2325 | text: key + ': ' + info[key], 2326 | fontFamily: 'Monaco', 2327 | fontFallback: 'monospace', 2328 | fontSize: 12, 2329 | textAlign: 'left', 2330 | textBaseline: 'alphabetic', 2331 | fillStyle: rgbWithOpacity([0, 0, 0], 1), 2332 | textPosition: { x: 5, y: height - fromBottom }, 2333 | }); 2334 | 2335 | fromBottom += 15; 2336 | } 2337 | }; 2338 | 2339 | self.draw = function () { 2340 | if (!DEBUG) draw({ type: 'clear' }); 2341 | 2342 | self.background.draw(); 2343 | self.drawScore(); 2344 | self.drawParticles(); 2345 | 2346 | if (self.started) self.wave.draw(); 2347 | self.cable.draw(); 2348 | self.tether.draw(); 2349 | self.player.draw(); 2350 | 2351 | self.drawLogo(); 2352 | self.drawRestartTutorial(); 2353 | 2354 | self.drawAchievementNotifications(); 2355 | 2356 | if (!self.started || self.ended) self.drawMuteButton(); 2357 | if (self.started && !self.ended) self.drawPlayButton(); 2358 | 2359 | if ((self.tether.lastInteraction === 'mouse' && self.ended) || !self.started) 2360 | self.drawAchievementUI(); 2361 | 2362 | if (INFO) self.drawInfo(); 2363 | }; 2364 | 2365 | self.end = function () { 2366 | if (document.pointerLockElement) document.exitPointerLock(); 2367 | logScore(self.score); 2368 | self.ended = self.timeElapsed; 2369 | self.tether.locked = true; 2370 | self.tether.unlockable = false; 2371 | self.setSpeed(self.slowSpeed); 2372 | }; 2373 | 2374 | self.reset(0); 2375 | } 2376 | 2377 | var enemyPool = [Drifter, Eye, Twitchy]; 2378 | 2379 | music = new Music(); 2380 | game = new Game(); 2381 | 2382 | function handleClick(e) { 2383 | if (game.eventShouldMute(e)) { 2384 | if (music.element.paused) { 2385 | console.log('play'); 2386 | music.element.play(); 2387 | saveCookie(musicMutedCookieKey, 'true'); 2388 | } else { 2389 | console.log('pause'); 2390 | music.element.pause(); 2391 | saveCookie(musicMutedCookieKey, 'false'); 2392 | } 2393 | } else if (game.eventShouldPlay(e)) { 2394 | paused = !paused; 2395 | } else if (game.ended) { 2396 | game.reset(0); 2397 | } 2398 | } 2399 | 2400 | var konamiLength = 0; 2401 | var konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'KeyB', 'KeyA', 'Space']; 2402 | 2403 | function konamiSeq(requiredKey, givenKey) { 2404 | if (requiredKey === givenKey) konamiLength++; 2405 | else konamiLength = 0; 2406 | 2407 | if (konamiLength === 11) { 2408 | subtitleText = 'Special Cheats Activated. Have fun!'; 2409 | playerRGB = 'Rainbow'; 2410 | } 2411 | } 2412 | 2413 | function handleKey(e) { 2414 | konamiSeq(konamiSequence[konamiLength], e.code); 2415 | if (self.started && !self.ended && e.code === 'KeyP') paused = !paused; 2416 | } 2417 | 2418 | document.addEventListener('click', handleClick); 2419 | document.addEventListener('keydown', handleKey); 2420 | 2421 | canvas.addEventListener('mousemove', function (e) { 2422 | if (game.tether.lastInteraction === 'touch' && document.pointerLockElement) 2423 | document.exitPointerLock(); 2424 | else if (document.pointerLockElement === canvas) { 2425 | if (game.tether.locked) game.tether.locked = false; 2426 | 2427 | game.lastMousePosition.x += e.movementX; 2428 | game.lastMousePosition.y += e.movementY; 2429 | 2430 | if (game.lastMousePosition.x < 0) game.lastMousePosition.x = 0; 2431 | else if (game.lastMousePosition.x > width) game.lastMousePosition.x = width; 2432 | 2433 | if (game.lastMousePosition.y < 0) game.lastMousePosition.y = 0; 2434 | else if (game.lastMousePosition.y > height) game.lastMousePosition.y = height; 2435 | } 2436 | }); 2437 | 2438 | document.addEventListener('touchstart', function (e) { 2439 | lastTouchStart = new Date().getTime(); 2440 | }); 2441 | document.addEventListener('touchend', function (e) { 2442 | if ( 2443 | lastTouchStart !== undefined && 2444 | new Date().getTime() - lastTouchStart < 300 2445 | ) { 2446 | handleClick(e); 2447 | } 2448 | }); 2449 | 2450 | window.requestFrame = 2451 | window.requestAnimationFrame || 2452 | window.webkitRequestAnimationFrame || 2453 | window.mozRequestAnimationFrame || 2454 | function (callback) { 2455 | window.setTimeout(callback, 1000 / 60); 2456 | }; 2457 | 2458 | var pauseDelay = 0; 2459 | function animate() { 2460 | requestFrame(animate); 2461 | if (!paused) { 2462 | game.step(); 2463 | if (pauseDelay !== 0) { 2464 | pauseDelay = 0; 2465 | if (canvas.requestPointerLock) canvas.requestPointerLock(); 2466 | game.player.teleportTo({ 2467 | x: game.lastMousePosition.x + 50, 2468 | y: game.lastMousePosition.y + 50, 2469 | }); 2470 | } 2471 | } else if (paused && pauseDelay !== 1) { 2472 | game.step(); 2473 | game.drawPauseMessage(); 2474 | if (document.pointerLockElement) document.exitPointerLock(); 2475 | pauseDelay++; 2476 | } 2477 | } 2478 | 2479 | var scrollTimeout; 2480 | window.addEventListener('scroll', function (e) { 2481 | clearTimeout(scrollTimeout); 2482 | scrollTimeout = setTimeout(function () { 2483 | window.scrollTo(0, 0); 2484 | }, 500); 2485 | }); 2486 | window.scrollTo(0, 0); 2487 | 2488 | animate(); -------------------------------------------------------------------------------- /source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | tether! 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /source/templates/main.template.html: -------------------------------------------------------------------------------- 1 | tether! -------------------------------------------------------------------------------- /source/templates/offline.template.html: -------------------------------------------------------------------------------- 1 | tether! -------------------------------------------------------------------------------- /splashscreens/ipad_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/ipad_splash.png -------------------------------------------------------------------------------- /splashscreens/ipadpro1_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/ipadpro1_splash.png -------------------------------------------------------------------------------- /splashscreens/ipadpro2_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/ipadpro2_splash.png -------------------------------------------------------------------------------- /splashscreens/ipadpro3_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/ipadpro3_splash.png -------------------------------------------------------------------------------- /splashscreens/iphone5_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphone5_splash.png -------------------------------------------------------------------------------- /splashscreens/iphone6_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphone6_splash.png -------------------------------------------------------------------------------- /splashscreens/iphoneplus_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphoneplus_splash.png -------------------------------------------------------------------------------- /splashscreens/iphonex_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphonex_splash.png -------------------------------------------------------------------------------- /splashscreens/iphonexr_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphonexr_splash.png -------------------------------------------------------------------------------- /splashscreens/iphonexsmax_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/splashscreens/iphonexsmax_splash.png -------------------------------------------------------------------------------- /tether.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tether! | Swing Around a Ball of Destruction!", 3 | "short_name": "tether!", 4 | "lang": "en-US", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#FFF", 8 | "description": "A game about swinging a ball around and sheer destruction.", 9 | "categories": [ 10 | "game", 11 | "mobile", 12 | "fun" 13 | ], 14 | "icons": [ 15 | { 16 | "src": "/icons/android-icon-36x36.png", 17 | "sizes": "36x36", 18 | "type": "image/png", 19 | "density": "0.75" 20 | }, 21 | { 22 | "src": "/icons/android-icon-48x48.png", 23 | "sizes": "48x48", 24 | "type": "image/png", 25 | "density": "1.0" 26 | }, 27 | { 28 | "src": "/icons/android-icon-72x72.png", 29 | "sizes": "72x72", 30 | "type": "image/png", 31 | "density": "1.5" 32 | }, 33 | { 34 | "src": "/icons/android-icon-96x96.png", 35 | "sizes": "96x96", 36 | "type": "image/png", 37 | "density": "2.0" 38 | }, 39 | { 40 | "src": "/icons/android-icon-144x144.png", 41 | "sizes": "144x144", 42 | "type": "image/png", 43 | "density": "3.0" 44 | }, 45 | { 46 | "src": "/icons/android-icon-192x192.png", 47 | "sizes": "192x192", 48 | "type": "image/png", 49 | "density": "4.0" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /tether_theme.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayhanadev/tether/5dcdec40cce5985d42a9ababa3733d758b09bc46/tether_theme.mp3 --------------------------------------------------------------------------------