├── .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 | 
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
--------------------------------------------------------------------------------