diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c6aa6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +other +package-lock.json +.vscode +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/README.md b/README.md index 06a5a80..d4aa4c8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,74 @@ # Kingdom Rush clone +Для проверки приложения можно воспользоваться нашим готовым деплоем - [netlify](https://kingdom-rush-rsclone.netlify.app/), либо развернуть весь бэк и фронт локально по инструкции ниже. +# RS School REST service -## Task -https://github.com/rolling-scopes-school/tasks/blob/master/tasks/rsclone/rsclone.md +## Prerequisites -## Team +- Git - [Download & Install Git](https://git-scm.com/downloads). +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. -## Description \ No newline at end of file +## Downloading + +``` +git clone {repository URL} +``` + +## Installing NPM modules + +``` +npm install +``` + +## Running application + +``` +npm start +``` + +After starting the app on port (4000 as default) you can open +in your browser OpenAPI documentation by typing http://localhost:4000/doc/. +For more information about OpenAPI/Swagger please visit https://swagger.io/. + +## Testing + +After application running open new terminal and enter: + +To run all tests without authorization + +``` +npm test +``` + +To run only one of all test suites (users, boards or tasks) + +``` +npm test +``` + +To run all test with authorization + +``` +npm run test:auth +``` + +To run only specific test suite with authorization (users, boards or tasks) + +``` +npm run test:auth +``` + +## Development + +If you're using VSCode, you can get a better developer experience from integration with [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extensions. + +### Auto-fix and format + +``` +npm run lint +``` + +### Debugging in VSCode + +Press F5 to debug. + +For more information, visit: https://code.visualstudio.com/docs/editor/debugging diff --git a/about.md b/about.md new file mode 100644 index 0000000..0aae755 --- /dev/null +++ b/about.md @@ -0,0 +1,93 @@ +# Kingdom Rush clone +
preview
+ +## Задание +https://github.com/rolling-scopes-school/tasks/blob/master/tasks/rsclone/rsclone.md + +## Команда +[Iogsotot](https://github.com/Iogsotot) |[mrINEX](https://github.com/mrINEX) |[Abdulloh76](https://github.com/Abdulloh76) |[DenisAfa](https://github.com/DenisAfa) | +:---:|:---:|:---:|:---:| +[Iogsotot](https://github.com/Iogsotot)|[mrINEX](https://github.com/mrINEX)|[Abdulloh76](https://github.com/Abdulloh76)|[DenisAfa](https://github.com/DenisAfa)| + +## Описание +В качестве финального задания мы выбрали сделать подобие игры Kingdom Rush - это tower defence. Главной игровой механикой стала защита определённого места на карте - “ворот” от прохождения через них врагов, для защиты игрок может строить башни на заранее отведенных участках карты. Подробнее о самой игре и ходе разработке можно почитат в статье - ([medium](https://ajustusa.medium.com/tower-defence-%D0%BD%D0%B0-phaser-3-%D1%83%D1%81%D0%BF%D0%B5%D1%88%D0%BD%D1%8B%D0%B9-%D0%BE%D0%BF%D1%8B%D1%82-%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D1%8F-%D0%B8%D0%B3%D1%80%D1%8B-%D0%B7%D0%B0-4-%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D0%B8-de4c8d1b570)) или посмотреть ниже в списке "Реализованные особенности" + +## Реализованные особенности + +### UI: +- [x] Есть возможность управление приложением/игрой с клавиатуры или наличие более пяти hot keys (20 баллов) +3-modal-1 и [видео](https://youtu.be/uPxPd6NfIbA) + +- [x] Есть возможность переключения 2 и более языков (10 баллов за каждый дополнительный язык, но не более 20 баллов) +image + +- [x] Есть хотя бы один модальный диалог (10 баллов) + 3-modal-2 3-modal-3 3-modal-4 3-modal-5 и [видео](https://youtu.be/9ehP_TxrBQA) + +- [x] Возможность кастомизации приложения, настроек пользователя (20 баллов) +3-modal-2 3-modal-5 и [видео](https://youtu.be/u7YnCUvZfE4) +- [x] Реализовано 3+ анимации, для создания которых используются ключевые кадры или svg-анимация (20 баллов) + + levendor scorpio orc + black-Wizzardи [видео](https://youtu.be/u7YnCUvZfE4) + +- [x] Приложение выполнено в едином стиле, для стилизации используется Bootstrap/Material UI/Ant design/etc (20 баллов) +interface6-ui-2 6-ui-3 6-ui interface и [видео](https://youtu.be/bm6VoKW5YiA) + + +### Работа игры: +- [x] Действие игры происходит на разных уровнях, картах, локациях, используются анимированные переходы между уровнями, анимации победы, поражения (30 баллов) +7-map-1 7-map-2 7-map-3 и [видео](https://youtu.be/tfT14xRkVjA) + +- [x] Расширенные настройки звука/видео/графики. Уровни громкости, язык озвучивания, вкл/выкл отображение теней, частиц (20 баллов) + 3-modal-5 и [видео](https://youtu.be/u7YnCUvZfE4) +- [x] Есть статистика, которая отображает прогресс игры, нанесенный урон, потраченное на игру время, процент выполнения задания или уровня etc (20 баллов) +9-stats-1 9-stats-2 9-stats-3 9-stats-4 и [видео](https://youtu.be/lZsZpRZms88) + +- [x] Написание логики для компьютерного противника (40 баллов) + 10-ii и [видео](https://youtu.be/U8HLtbB2-n8) + +## Технический стек: + +- [x] Использован Canvas/WebGL/etc (20 баллов) + 11-stack-canvas-1 11-stack-Web-GL +- [x] Использован webpack (10 баллов) + 12-stack-webpack и [видео](https://youtu.be/bC6POPzrYaM) + +- [x] Сохранение и загрузка чего-либо с использованием Local storage (10 баллов) + 13-stack-LS и [видео](https://youtu.be/fzZKs2HTo5g) +- [x] Приложение/игра написанны на TypeScript (40 баллов) + 14-stack-TS и [видео](https://youtu.be/RBZJqAvYYPw) + +## Работа с кодом: +- [x] Использован eslint, eslint-config-airbnb-base (10 баллов) + 15-eslint +- [x] Понятный, читаемый код. Имена переменных и функций отражают то что в них содержится/то что они делают. Функция выполняет одно действие. Повторение логики сведено к минимуму. (20 баллов) + issues-24-01 `мы очень старались и много рефачили код` +## Back-end: +Одно видео на весь бэк -[тык!](https://youtu.be/hRccVLDlcpk) +- [x] Использован RESTful API (30 баллов) + +- [x] Подключение и работа с БД (30 баллов) +- [x] Аутентификация (20 баллов) +- [x] Приложение отображает какую-либо статистику/графики/таблицы, данные для которых получает от бекенда (20 баллов) +- [x] Реализован nodejs и express, отдаёт корректные ответы, отдаёт HTTP ошибки с нормальными body, по которым можно понять, что произошло, пишет читаемые логи (40 балов) +`скринов гора, они между собой перекликаются, поэтому путь за весь бэк все скрины разом:` +photo-2021-02-02-13-33-59 photo-2021-02-02-13-41-41 photo-2021-02-02-13-42-39 photo-2021-02-03-14-31-48 photo-2021-02-03-14-32-33 photo-2021-02-03-14-38-43 photo-2021-02-03-14-39-02 photo-2021-02-03-14-43-43 photo-2021-02-03-14-45-35 photo-2021-02-03-14-46-17 image image image + +## Пункты из таблички, которые нам тоже подходят: +- [x] Многопоточность (40 баллов) +`у нас одновременно может играть сколько угодно игроков и статистика об их достижениях будет постоянно попадать на бэк, а оттуда улетать ко всем остальным игрокам(настроена переодичность полной синхронизации всех данных)` +- [x] Сохранение и загрузка игры (10 баллов) +`В нашей игре прогресс сохраняется, если человек зайдет даже с другого компьютера - он сможет продолжить защищать Линерию с того уровня на котором закончил` +- [x] асинхронная работа с бэкендом (40 баллов) +`реализовано в полной мере` +- [x] саморисованный дизайн (40 баллов) +`реализовано в полной мере` +interface6-ui-2 6-ui-3 6-ui interface и [видео](https://youtu.be/bm6VoKW5YiA) +- [x] Реализовано переключение экранов игры (10 баллов) +`FideIn, FideOut, анимированное появление (Cubic) и всё остальное, что есть у приличных игр` +в [табличке](https://docs.google.com/spreadsheets/d/11023uEpgkxCt-AuiF_BnA4_KbilPK_5mj2F4oPhF5tU/edit#gid=0) ещё много подходящих нам пунктов, но по-моему нам и без неё хорошо. Мы надеемся, что наша игра вам понравится, мы очень старались сделать её хорошей со всех сторон. + +## Итого: 470 баллов (по изначальному ТЗ) и 150+ баллов (за пункты из таблички) +`+120 за статью - там и картинки, и схемы, и видео, и даже на грамотность проверено всё =)` \ No newline at end of file diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 0000000..1c15c98 --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,332 @@ +{ + "env": { + "browser": true, + "node": true, + "jasmine": true, + "es6": true, + "jest": true + }, + "parserOptions": { + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": false, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "restParams": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true + } + }, + "extends": [ + "eslint:recommended", + "plugin:node/recommended", + "plugin:prettier/recommended" + ], + "rules": { + "no-debugger": 1, + // Possible errors + "comma-dangle": [ + 2, + "never" + ], + "no-cond-assign": [ + 2, + "always" + ], + "no-constant-condition": 2, + "no-control-regex": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-empty": 2, + "no-extra-boolean-cast": 0, + "no-extra-parens": [ + 2, + "functions" + ], + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-regex-spaces": 2, + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-typeof": 2, + "no-unexpected-multiline": 0, + // Best Practices + "block-scoped-var": 2, + "complexity": [ + 2, + 50 + ], + "curly": [ + 2, + "multi-line" + ], + "default-case": 2, + "dot-notation": [ + 2, + { + "allowKeywords": true, + "allowPattern": "^([a-z]+(_[a-z]+)+)|[A-Z]+|[A-Z]{1}[a-z]+$" + } + ], + "eqeqeq": 2, + "guard-for-in": 2, + "no-alert": 1, + "no-caller": 2, + "no-case-declarations": 2, + "no-div-regex": 0, + "no-else-return": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 0, + "no-proto": 2, + "no-redeclare": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-unused-expressions": [ + 2, + { + "allowShortCircuit": true, + "allowTernary": false + } + ], + "no-useless-call": 2, + "no-with": 2, + "radix": 2, + "wrap-iife": [ + 2, + "outside" + ], + "yoda": 2, + // ES2015 + "arrow-parens": 0, + "arrow-spacing": [ + 2, + { + "before": true, + "after": true + } + ], + "constructor-super": 2, + "no-class-assign": 2, + "no-const-assign": 2, + "no-this-before-super": 0, + "no-var": 2, + "object-shorthand": [ + 2, + "always" + ], + "prefer-arrow-callback": 2, + "prefer-const": 2, + "prefer-spread": 2, + "prefer-template": 2, + // Strict Mode + "strict": [ + 2, + "never" + ], + // Variables + "no-catch-shadow": 2, + "no-delete-var": 2, + "no-label-var": 2, + "no-shadow-restricted-names": 2, + "no-shadow": 2, + "no-undef-init": 2, + "no-undef": 2, + "no-unused-vars": 2, + // Node.js + "callback-return": 2, + "no-mixed-requires": 2, + "no-path-concat": 2, + "no-sync": 2, + "handle-callback-err": 1, + "no-new-require": 2, + // Stylistic + "array-bracket-spacing": [ + 2, + "never", + { + "singleValue": false, + "objectsInArrays": false, + "arraysInArrays": false + } + ], + "newline-after-var": 0, + "brace-style": [ + 2, + "1tbs" + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [ + 2, + "last" + ], + "computed-property-spacing": [ + 2, + "never" + ], + "eol-last": 2, + "func-names": 1, + "func-style": [ + 2, + "declaration", + { + "allowArrowFunctions": true + } + ], + "linebreak-style": 0, + "max-nested-callbacks": [ + 2, + 4 + ], + "new-parens": 2, + "no-array-constructor": 2, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [ + 2, + { + "max": 2, + "maxEOF": 1 + } + ], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-unneeded-ternary": 2, + "object-curly-spacing": [ + 2, + "always" + ], + "one-var": [ + 2, + "never" + ], + "padded-blocks": [ + 2, + "never" + ], + "quotes": [ + 1, + "single", + "avoid-escape" + ], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "semi": [ + 2, + "always" + ], + "keyword-spacing": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + { + "anonymous": "always", + "named": "never" + } + ], + "space-in-parens": [ + 2, + "never" + ], + "space-infix-ops": 2, + "space-unary-ops": [ + 2, + { + "words": true, + "nonwords": false + } + ], + "spaced-comment": [ + 2, + "always", + { + "exceptions": [ + "-", + "+" + ], + "markers": [ + "=", + "!" + ] + } + ], + // Legacy + "max-depth": [ + 0, + 4 + ], + "max-params": [ + 2, + 7 + ], + "no-bitwise": 2 + }, + "globals": { + "$": true, + "ga": true, + "__ENV__": true, + "__DEVTOOLS__": true, + "PUBLIC_URL": true, + "jestExpect": true + }, + "overrides": [ + { + "files": [ + "test/**/*.js" + ], + "rules": { + "node/no-unpublished-require": "off", + "max-nested-callbacks": [ + 2, + 10 + ] + } + } + ] +} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..6a8af5e --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0180168 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,33 @@ +# Kingdom Rush clone REST service + +## Prerequisites + +- Git - [Download & Install Git](https://git-scm.com/downloads). +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. + +``` +cd backend +``` +Создайте файл `.env ` в корне приложения + +В созданном файе укажите переменные окружения: + +``` +PORT=<порт на котором будет запущено приложение> +MONGO_CONNECTION_STRING=<адрес вашей локальной или облачной mongodb> +JWT_SECRET_KEY=<ваш секретный ключ для подписи JWT> +``` + +## Installing NPM modules + +``` +npm install +``` + +## Running application + +``` +npm startnodemon +``` + + diff --git a/backend/doc/api.yaml b/backend/doc/api.yaml new file mode 100644 index 0000000..31a22ed --- /dev/null +++ b/backend/doc/api.yaml @@ -0,0 +1,232 @@ +openapi: 3.0.0 +info: + title: Trello Service + description: Let's try to create a competitor for Trello! + version: 1.0.0 + +servers: + - url: / + +components: + schemas: + User: + type: object + properties: + id: + type: string + name: + type: string + login: + type: string + responses: + UnauthorizedError: + description: Access token is missing or invalid + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +security: + - bearerAuth: [] + +paths: + /users: + get: + tags: + - Users + summary: Get all users + description: Gets all users (remove password from response) + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + 401: + $ref: '#/components/responses/UnauthorizedError' + post: + tags: + - Users + summary: Create user + description: Creates a new user (remove password from response) + requestBody: + required: true + content: + application/json: + schema: + type: object + title: example + properties: + name: + type: string + description: The user's name + login: + type: string + description: The user's login + password: + type: string + description: The user's password + required: + - name + responses: + 200: + description: The user has been created. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + 400: + description: Bad request + 401: + $ref: '#/components/responses/UnauthorizedError' + /users/{userId}: + parameters: + - name: userId + in: path + required: true + schema: + type: string + get: + tags: + - Users + summary: Get user by ID + description: Gets a user by ID + e.g. “/users/123” (remove password from response) + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/User" + 401: + $ref: '#/components/responses/UnauthorizedError' + 404: + description: User not found + put: + tags: + - Users + summary: Update a user + description: Updates a user by ID + requestBody: + required: true + content: + application/json: + schema: + type: object + title: example + properties: + name: + type: string + description: The user's name + login: + type: string + description: The user's login + password: + type: string + description: The user's password + required: + - name + responses: + 200: + description: The user has been updated. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + 400: + description: Bad request + 401: + $ref: '#/components/responses/UnauthorizedError' + delete: + tags: + - Users + summary: Delete user + description: Deletes user by ID. When somebody + DELETE User, all Tasks where User is assignee + should be updated to put userId=null + responses: + 204: + description: The user has been deleted + 401: + $ref: '#/components/responses/UnauthorizedError' + 404: + description: User not found + /login: + post: + tags: + - Login + security: + [] + summary: Login + description: Logins a user and returns a JWT-token + requestBody: + required: true + content: + application/json: + schema: + type: object + title: example + properties: + login: + type: string + description: Username + password: + type: string + description: Password + required: + - user + - login + responses: + 200: + description: Successful login. + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: JWT Token + 403: + description: Incorrect login or password + /chart: + put: + tags: + - Users + summary: Update a user + description: Updates a user by ID + requestBody: + required: true + content: + application/json: + schema: + type: object + title: example + properties: + name: + type: string + description: The user's name + login: + type: string + description: The user's login + password: + type: string + description: The user's password + required: + - name + responses: + 200: + description: The user has been updated. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + 400: + description: Bad request + 401: + $ref: '#/components/responses/UnauthorizedError' diff --git a/backend/doc/connection.png b/backend/doc/connection.png new file mode 100644 index 0000000..75537e4 Binary files /dev/null and b/backend/doc/connection.png differ diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..397c694 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,75 @@ +{ + "name": "rest-service-rsclone", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "" + }, + "type": "commonjs", + "description": "REST service for rsclone", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "startnodemon": "nodemon src/server.js", + "lint": "eslint ./ --ignore-path .gitignore --fix", + "test": "cross-env DEBUG=rs:* jest --testMatch \"/test/e2e/test/*.test.js\" --noStackTrace --runInBand", + "test:auth": "cross-env DEBUG=rs:* TEST_MODE=auth jest --noStackTrace" + }, + "keywords": [ + "rs", + "school", + "rest", + "node", + "express", + "autotest", + "starter" + ], + "license": "ISC", + "engines": { + "node": ">=12.0.0" + }, + "jest": { + "testEnvironment": "node", + "setupFilesAfterEnv": [ + "./test/setup.js" + ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "git add" + ] + }, + "dependencies": { + "cors": "^2.8.5", + "cross-env": "6.0.3", + "dotenv": "8.2.0", + "express": "4.17.1", + "helmet": "^4.3.1", + "jsonwebtoken": "^8.5.1", + "mongoose": "^5.11.8", + "swagger-ui-express": "4.1.2", + "uuid": "3.3.3", + "yamljs": "0.3.0" + }, + "devDependencies": { + "eslint": "6.7.2", + "eslint-config-prettier": "6.7.0", + "eslint-plugin-node": "10.0.0", + "eslint-plugin-prettier": "3.1.1", + "husky": "3.1.0", + "lint-staged": "9.5.0", + "nodemon": "2.0.2", + "prettier": "1.19.1", + "chai": "4.2.0", + "debug": "4.1.1", + "dirty-chai": "2.0.1", + "jest": "24.9.0", + "supertest": "4.0.2" + } +} diff --git a/backend/src/app.js b/backend/src/app.js new file mode 100644 index 0000000..f7b5be0 --- /dev/null +++ b/backend/src/app.js @@ -0,0 +1,44 @@ +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const swaggerUI = require('swagger-ui-express'); +const path = require('path'); +const YAML = require('yamljs'); + +const loginRouter = require('./resources/login/login.router'); +const logupRouter = require('./resources/logup/logup.router'); +const userRouter = require('./resources/users/user.router'); +const chartRouter = require('./resources/chart/chart.router'); +const statsRouter = require('./resources/stats/stats.router'); + +const app = express(); +const swaggerDocument = YAML.load(path.join(__dirname, '../doc/api.yaml')); +const middlewareAuth = require('./resources/login/login.middleware'); + +app.use(helmet()); +app.use(cors()); +app.use(express.json()); + +app.use('/doc', swaggerUI.serve, swaggerUI.setup(swaggerDocument)); + +app.use('/', (req, res, next) => { + if (req.originalUrl === '/') { + res.send('Service is running!'); + return; + } + next(); +}); + +app.use('/login', loginRouter); + +app.use('/logup', logupRouter); + +app.use('/chart', chartRouter); + +app.use(middlewareAuth); + +app.use('/users', userRouter); + +userRouter.use('/:id/stats', statsRouter); + +module.exports = app; diff --git a/backend/src/common/config.js b/backend/src/common/config.js new file mode 100644 index 0000000..6c6a37f --- /dev/null +++ b/backend/src/common/config.js @@ -0,0 +1,14 @@ +const dotenv = require('dotenv'); +const path = require('path'); + +dotenv.config({ + path: path.join(__dirname, '../../.env') +}); + +module.exports = { + PORT: process.env.PORT, + NODE_ENV: process.env.NODE_ENV, + MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING, + JWT_SECRET_KEY: process.env.JWT_SECRET_KEY, + AUTH_MODE: process.env.AUTH_MODE === 'true' +}; diff --git a/backend/src/common/db.client.js b/backend/src/common/db.client.js new file mode 100644 index 0000000..e15611f --- /dev/null +++ b/backend/src/common/db.client.js @@ -0,0 +1,20 @@ +const { MONGO_CONNECTION_STRING } = require('./config'); +const mongoose = require('mongoose'); + +function connectToDB(cb) { + mongoose.connect(MONGO_CONNECTION_STRING, { + useNewUrlParser: true, + useUnifiedTopology: true + }); + + const db = mongoose.connection; + db.on('error', console.error.bind(console, 'connection error:')); + db.once('open', () => { + console.log('We are connected to mongoDB.'); + cb(); + }); +} + +module.exports = { + connectToDB +}; diff --git a/backend/src/common/handling.js b/backend/src/common/handling.js new file mode 100644 index 0000000..7ac97ca --- /dev/null +++ b/backend/src/common/handling.js @@ -0,0 +1,36 @@ +function recordError(template) { + console.error('Error:', template); + throw new Error(template); +} + +function handling(fn) { + return async (req, res) => { + const timeDate = `time: ${new Date()}`; + const url = `\nurl: ${req.originalUrl}`; + const method = `\nmethod: ${req.method}`; + const query = `\nquery: ${JSON.stringify(req.query, null, 2)}`; + let body; + if (req.body.password) { + const hidePass = req.body.password.replace(/./g, '*'); + body = `\nbody: ${JSON.stringify( + { ...req.body, password: hidePass }, + null, + 2 + )}`; + } else { + body = `\nbody: ${JSON.stringify(req.body, null, 2)}`; + } + const params = `\nparams: ${JSON.stringify(req.params, null, 2)}`; + + const logging = `${timeDate}${url}${method}${query}${params}${body}\n\n`; + + process.stdout.write(logging); + + fn.call(this, req, res); + }; +} + +module.exports = { + handling, + recordError +}; diff --git a/backend/src/resources/chart/chart.model.js b/backend/src/resources/chart/chart.model.js new file mode 100644 index 0000000..776cb96 --- /dev/null +++ b/backend/src/resources/chart/chart.model.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); + +const chartScheme = new mongoose.Schema( + { + id: String, + attendance: Number, + date: String + }, + { versionKey: false } +); + +const Chart = mongoose.model('Chart', chartScheme); + +module.exports = Chart; diff --git a/backend/src/resources/chart/chart.mongo.repository.js b/backend/src/resources/chart/chart.mongo.repository.js new file mode 100644 index 0000000..223318f --- /dev/null +++ b/backend/src/resources/chart/chart.mongo.repository.js @@ -0,0 +1,19 @@ +const Chart = require('./chart.model'); + +const getAll = async () => { + return Chart.find({}); +}; + +const get = async id => { + return Chart.findOne({ id }); +}; + +const create = async body => { + return Chart.create(body); +}; + +const update = async (id, body) => { + return Chart.updateOne({ id }, body); +}; + +module.exports = { getAll, create, get, update }; diff --git a/backend/src/resources/chart/chart.router.js b/backend/src/resources/chart/chart.router.js new file mode 100644 index 0000000..5ad1405 --- /dev/null +++ b/backend/src/resources/chart/chart.router.js @@ -0,0 +1,43 @@ +const router = require('express').Router(); +const chartService = require('./chart.service'); +const { handling } = require('../../common/handling'); + +router.route('/').get( + handling(async (req, res) => { + const response = await chartService.getAll(); + res.send({ data: response, ok: true }); + }) +); + +router.route('/:id').get( + handling(async (req, res) => { + const response = await chartService.get(req.params.id); + + if (response) { + res.send({ data: response, ok: true }); + } else { + res.send({ data: 'No Content', ok: false }); + } + }) +); + +router.route('/').post( + handling(async (req, res) => { + const response = await chartService.create(req.body); + res.send({ data: response, ok: true }); + }) +); + +router.route('/:id').put( + handling(async (req, res) => { + const response = await chartService.update(req.params.id, req.body); + + if (!response.nModified) { + res.send({ data: 'No Content', ok: false }); + } else { + res.status(302).send({ data: 'Found', ok: true }); + } + }) +); + +module.exports = router; diff --git a/backend/src/resources/chart/chart.service.js b/backend/src/resources/chart/chart.service.js new file mode 100644 index 0000000..c18730d --- /dev/null +++ b/backend/src/resources/chart/chart.service.js @@ -0,0 +1,11 @@ +const usersRepo = require('./chart.mongo.repository'); + +const getAll = () => usersRepo.getAll(); + +const get = id => usersRepo.get(id); + +const create = id => usersRepo.create(id); + +const update = (id, body) => usersRepo.update(id, body); + +module.exports = { getAll, create, get, update }; diff --git a/backend/src/resources/login/login.middleware.js b/backend/src/resources/login/login.middleware.js new file mode 100644 index 0000000..adbb930 --- /dev/null +++ b/backend/src/resources/login/login.middleware.js @@ -0,0 +1,34 @@ +const jwt = require('jsonwebtoken'); +const { JWT_SECRET_KEY } = require('../../common/config'); + +function middlewareAuth(req, res, next) { + try { + const auth = req.header('Authorization'); + if (!auth) { + return res.status(401).send({ + data: 'Wrong authenticate scheme!', + ok: false + }); + } + + const [type, token] = auth.split(' '); + if (type !== 'Bearer') { + return res.status(401).send({ + data: 'Wrong authenticate scheme!', + ok: false + }); + } + + jwt.verify(token, JWT_SECRET_KEY); + + return next(); + } catch (err) { + console.log('Error: ', err.message); + res.status(500).send({ + data: 'Invalid Token', + ok: false + }); + } +} + +module.exports = middlewareAuth; diff --git a/backend/src/resources/login/login.router.js b/backend/src/resources/login/login.router.js new file mode 100644 index 0000000..8368a50 --- /dev/null +++ b/backend/src/resources/login/login.router.js @@ -0,0 +1,25 @@ +const router = require('express').Router(); +const { handling } = require('../../common/handling'); +const loginService = require('./login.service'); + +router.route('/').post( + handling(async (req, res) => { + const { login, password } = req.body; + const user = await loginService.getUser(login); + + if (!user) { + res.send({ data: 'No Content', ok: false }); + } else if (password !== user.password) { + res.send({ data: 'Invalid password', ok: false }); + } else { + res.status(200).json({ + token: loginService.createToken(login, password), + id: user.id, + login: user.login, + ok: true + }); + } + }) +); + +module.exports = router; diff --git a/backend/src/resources/login/login.service.js b/backend/src/resources/login/login.service.js new file mode 100644 index 0000000..ad772d0 --- /dev/null +++ b/backend/src/resources/login/login.service.js @@ -0,0 +1,16 @@ +const jwt = require('jsonwebtoken'); +const { JWT_SECRET_KEY } = require('../../common/config'); +const User = require('../users/user.model'); + +const getUser = async login => { + return User.findOne({ login }); +}; + +const createToken = (login, password) => { + return jwt.sign({ login, password }, JWT_SECRET_KEY); +}; + +module.exports = { + getUser, + createToken +}; diff --git a/backend/src/resources/logup/logup.router.js b/backend/src/resources/logup/logup.router.js new file mode 100644 index 0000000..5cc3251 --- /dev/null +++ b/backend/src/resources/logup/logup.router.js @@ -0,0 +1,19 @@ +const router = require('express').Router(); +const { handling } = require('../../common/handling'); +const usersService = require('../users/user.service'); +const User = require('../users/user.model'); + +router.route('/').post( + handling(async (req, res) => { + const user = await usersService.getByLogin(req.body.login); + + if (user) { + res.send({ data: 'user with this login exists', ok: false }); + } else { + const userNew = await usersService.create(req.body); + res.status(200).send({ data: User.toResponse(userNew), ok: true }); + } + }) +); + +module.exports = router; diff --git a/backend/src/resources/stats/stats.model.js b/backend/src/resources/stats/stats.model.js new file mode 100644 index 0000000..5cc97e0 --- /dev/null +++ b/backend/src/resources/stats/stats.model.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); + +const statsScheme = new mongoose.Schema( + { + userId: { type: String }, + login: { type: String }, + gameProgress: { + level_1: { type: Number, default: 0 }, + level_2: { type: Number, default: 0 }, + level_3: { type: Number, default: 0 } + }, + gameLogInCount: { type: Number, default: 1 }, + killedEnemies: { type: Number, default: 0 }, + builtTowers: { type: Number, default: 0 }, + soldTowers: { type: Number, default: 0 }, + ironModeProgress: { + level_1: { type: Number, default: 0 }, + level_2: { type: Number, default: 0 }, + level_3: { type: Number, default: 0 } + }, + achievements: { + firstAsterisk: { type: Boolean, default: false }, + completeWin: { type: Boolean, default: false }, + firstBlood: { type: Boolean, default: false }, + greatDefender: { type: Boolean, default: false }, + ironDefender: { type: Boolean, default: false }, + killer: { type: Boolean, default: false }, + seller: { type: Boolean, default: false }, + builder: { type: Boolean, default: false } + } + }, + { versionKey: false } +); + +const Stats = mongoose.model('Stats', statsScheme); + +module.exports = Stats; diff --git a/backend/src/resources/stats/stats.mongodb.repository.js b/backend/src/resources/stats/stats.mongodb.repository.js new file mode 100644 index 0000000..6cfb09e --- /dev/null +++ b/backend/src/resources/stats/stats.mongodb.repository.js @@ -0,0 +1,24 @@ +const Stats = require('./stats.model'); + +const getAll = async () => { + return Stats.find({}); +}; + +const get = async userId => { + return Stats.findOne({ userId }); +}; + +const create = async body => { + return Stats.create(body); +}; + +const update = async (userId, body) => { + return Stats.updateOne({ userId }, body); +}; + +const remove = async userId => { + const res = await Stats.deleteOne({ userId }); + return res; +}; + +module.exports = { getAll, create, get, update, remove }; diff --git a/backend/src/resources/stats/stats.router.js b/backend/src/resources/stats/stats.router.js new file mode 100644 index 0000000..6aeac55 --- /dev/null +++ b/backend/src/resources/stats/stats.router.js @@ -0,0 +1,65 @@ +const express = require('express'); +const router = express.Router({ mergeParams: true }); +const statsService = require('./stats.service'); +const { handling } = require('../../common/handling'); + +router.route('/').get( + handling(async (req, res) => { + const stats = await statsService.getAll(); + res.send({ data: stats, ok: true }); + }) +); + +router.route('/').post( + handling(async (req, res) => { + const stat = await statsService.get(req.params.id); + + if (stat) { + res.send({ data: 'No Content', ok: false }); + } else { + const stats = await statsService.create({ + userId: req.params.id, + login: req.body.login + }); + res.send({ data: stats, ok: true }); + } + }) +); + +router.route('/current').get( + handling(async (req, res) => { + const stat = await statsService.get(req.params.id); + + if (stat) { + res.send({ data: stat, ok: true }); + } else { + res.send({ data: 'No Content', ok: false }); + } + }) +); + +router.route('/').put( + handling(async (req, res) => { + const stat = await statsService.update(req.params.id, req.body); + + if (!stat.nModified) { + res.send({ data: 'No Content', ok: false }); + } else { + res.status(302).send({ data: 'Found', ok: true }); + } + }) +); + +router.route('/').delete( + handling(async (req, res) => { + const stat = await statsService.remove(req.params.id); + + if (!stat.deletedCount) { + res.send({ data: 'No Content', ok: false }); + } else { + res.send({ ok: true, data: `delete successful ${stat.deletedCount}` }); + } + }) +); + +module.exports = router; diff --git a/backend/src/resources/stats/stats.service.js b/backend/src/resources/stats/stats.service.js new file mode 100644 index 0000000..47773e3 --- /dev/null +++ b/backend/src/resources/stats/stats.service.js @@ -0,0 +1,13 @@ +const tasksRepo = require('./stats.mongodb.repository'); + +const getAll = () => tasksRepo.getAll(); + +const get = id => tasksRepo.get(id); + +const create = user => tasksRepo.create(user); + +const update = (id, body) => tasksRepo.update(id, body); + +const remove = id => tasksRepo.remove(id); + +module.exports = { getAll, create, get, update, remove }; diff --git a/backend/src/resources/users/user.model.js b/backend/src/resources/users/user.model.js new file mode 100644 index 0000000..a843041 --- /dev/null +++ b/backend/src/resources/users/user.model.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); +const uuid = require('uuid'); + +const userScheme = new mongoose.Schema( + { + id: { + type: String, + default: uuid + }, + login: String, + password: String + }, + { versionKey: false } +); + +userScheme.static('toResponse', user => { + const { id, login } = user; + return { id, login }; +}); + +const User = mongoose.model('User', userScheme); + +module.exports = User; diff --git a/backend/src/resources/users/user.mongo.repository.js b/backend/src/resources/users/user.mongo.repository.js new file mode 100644 index 0000000..2d0fdb0 --- /dev/null +++ b/backend/src/resources/users/user.mongo.repository.js @@ -0,0 +1,28 @@ +const User = require('./user.model'); + +const getAll = async () => { + return User.find({}); +}; + +const get = async id => { + return User.findOne({ id }); +}; + +const getByLogin = async login => { + return User.findOne({ login }); +}; + +const create = async body => { + return User.create(body); +}; + +const update = async (id, body) => { + return User.updateOne({ id }, body); +}; + +const remove = async id => { + const res = await User.deleteOne({ id }); + return res; +}; + +module.exports = { getAll, create, get, getByLogin, update, remove }; diff --git a/backend/src/resources/users/user.router.js b/backend/src/resources/users/user.router.js new file mode 100644 index 0000000..bebaef7 --- /dev/null +++ b/backend/src/resources/users/user.router.js @@ -0,0 +1,64 @@ +const router = require('express').Router(); +const User = require('./user.model'); +const usersService = require('./user.service'); +const { handling } = require('../../common/handling'); + +router.route('/').get( + handling(async (req, res) => { + const users = await usersService.getAll(); + res.send({ data: users.map(User.toResponse), ok: true }); + }) +); + +router.route('/:id').get( + handling(async (req, res) => { + const user = await usersService.get(req.params.id); + if (user) { + res.send({ data: User.toResponse(user), ok: true }); + } else { + res.send({ data: 'No Content', ok: false }); + } + }) +); + +router.route('/').post( + handling(async (req, res) => { + const user = await usersService.getByLogin(req.body.login); + + if (user) { + res.send({ data: 'user with this login exists', ok: false }); + } else { + const userNew = await usersService.create(req.body); + res.status(200).send({ data: User.toResponse(userNew), ok: true }); + } + }) +); + +router.route('/:id').put( + handling(async (req, res) => { + const user = await usersService.update(req.params.id, req.body); + + if (!user.nModified) { + res.send({ data: 'Not found', ok: false }); + } else { + const userOld = await usersService.get(req.params.id); + res.status(200).send({ data: User.toResponse(userOld), ok: true }); + } + }) +); + +router.route('/:id').delete( + handling(async (req, res) => { + const user = await usersService.remove(req.params.id); + + if (!user.deletedCount) { + res.send({ data: 'Not found', ok: false }); + } else { + res + .status(200) + .send({ ok: true, data: `delete successful ${user.deletedCount}` }); + } + }) +); + +module.exports = router; diff --git a/backend/src/resources/users/user.service.js b/backend/src/resources/users/user.service.js new file mode 100644 index 0000000..8a186b3 --- /dev/null +++ b/backend/src/resources/users/user.service.js @@ -0,0 +1,15 @@ +const usersRepo = require('./user.mongo.repository'); + +const getAll = () => usersRepo.getAll(); + +const get = id => usersRepo.get(id); + +const getByLogin = login => usersRepo.getByLogin(login); + +const create = user => usersRepo.create(user); + +const update = (id, user) => usersRepo.update(id, user); + +const remove = id => usersRepo.remove(id); + +module.exports = { getAll, create, get, getByLogin, update, remove }; diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 0000000..6829e16 --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,26 @@ +const { connectToDB } = require('./common/db.client'); +const { recordError } = require('./common/handling'); +const { PORT } = require('./common/config'); +const app = require('./app'); + +connectToDB(() => { + app.listen(PORT, () => + console.log(`App is running on http://localhost:${PORT} ---- ${new Date()}`) + ); + + process.on('uncaughtException', err => { + const template = `Uncaught Exception at: ${err}\n\n`; + + recordError(template); + + console.error(`Uncaught Exception at: ${err}`); + }); + + process.on('unhandledRejection', reason => { + const template = `Unhandled Rejection at: '${reason}\n\n`; + + recordError(template); + + console.error('Unhandled Rejection at: ', reason.name, reason.message); + }); +}); diff --git a/backend/test/e2e/lib/index.js b/backend/test/e2e/lib/index.js new file mode 100644 index 0000000..fe15531 --- /dev/null +++ b/backend/test/e2e/lib/index.js @@ -0,0 +1,14 @@ +const supertest = require('supertest'); +const debug = require('debug')('rs:lib'); + +const routes = require('./routes'); + +const host = process.env.HOST || 'localhost:4000'; +debug('HOST', host); + +const request = supertest(host); + +module.exports = { + request, + routes +}; diff --git a/backend/test/e2e/lib/routes.js b/backend/test/e2e/lib/routes.js new file mode 100644 index 0000000..20a67c9 --- /dev/null +++ b/backend/test/e2e/lib/routes.js @@ -0,0 +1,24 @@ +module.exports = { + users: { + getAll: '/users', + getById: id => `/users/${id}`, + create: '/users', + update: id => `/users/${id}`, + delete: id => `/users/${id}` + }, + tasks: { + getAll: boardId => `/boards/${boardId}/tasks`, + getById: (boardId, taskId) => `/boards/${boardId}/tasks/${taskId}`, + create: boardId => `/boards/${boardId}/tasks`, + update: (boardId, taskId) => `/boards/${boardId}/tasks/${taskId}`, + delete: (boardId, taskId) => `/boards/${boardId}/tasks/${taskId}` + }, + boards: { + getAll: '/boards', + getById: id => `/boards/${id}`, + create: '/boards', + update: id => `/boards/${id}`, + delete: id => `/boards/${id}` + }, + login: '/login' +}; diff --git a/backend/test/e2e/test-auth/boards.test.js b/backend/test/e2e/test-auth/boards.test.js new file mode 100644 index 0000000..d685279 --- /dev/null +++ b/backend/test/e2e/test-auth/boards.test.js @@ -0,0 +1,48 @@ +const { request, routes } = require('../lib'); + +const TEST_BOARD_DATA = { + title: 'Autotest board', + columns: [ + { title: 'Backlog', order: 1 }, + { title: 'Sprint', order: 2 } + ] +}; +describe('Boards suite', () => { + describe('GET all boards', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.boards.getAll).expect(401); + }); + }); + + describe('GET board by id', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.boards.getById('12345')).expect(401); + }); + }); + + describe('POST', () => { + it('should get 401 without token presented ', async () => { + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .expect(401); + }); + }); + + describe('PUT', () => { + it('should get 401 without token presented ', async () => { + await request + .put(routes.boards.update('12345')) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .expect(401); + }); + }); + + describe('DELETE', () => { + it('should get 401 without token presented ', async () => { + await request.delete(routes.boards.delete('12345')).expect(401); + }); + }); +}); diff --git a/backend/test/e2e/test-auth/tasks.test.js b/backend/test/e2e/test-auth/tasks.test.js new file mode 100644 index 0000000..372dd2c --- /dev/null +++ b/backend/test/e2e/test-auth/tasks.test.js @@ -0,0 +1,50 @@ +const { request, routes } = require('../lib'); + +const TEST_TASK_DATA = { + title: 'Autotest task', + order: 0, + description: 'Lorem ipsum', + userId: null, + boardId: null, + columnId: null +}; + +describe('Tasks suite', () => { + describe('GET all', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.tasks.getAll('12345')).expect(401); + }); + }); + + describe('GET by id', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.tasks.getById('12345', '12345')).expect(401); + }); + }); + + describe('POST', () => { + it('should get 401 without token presented ', async () => { + await request + .post(routes.tasks.create('12345')) + .set('Accept', 'application/json') + .send(TEST_TASK_DATA) + .expect(401); + }); + }); + + describe('PUT', () => { + it('should get 401 without token presented ', async () => { + await request + .put(routes.tasks.update('12345', '12345')) + .set('Accept', 'application/json') + .send(TEST_TASK_DATA) + .expect(401); + }); + }); + + describe('DELETE', () => { + it('should get 401 without token presented ', async () => { + await request.delete(routes.tasks.delete('12345', '12345')).expect(401); + }); + }); +}); diff --git a/backend/test/e2e/test-auth/users.test.js b/backend/test/e2e/test-auth/users.test.js new file mode 100644 index 0000000..614ea22 --- /dev/null +++ b/backend/test/e2e/test-auth/users.test.js @@ -0,0 +1,45 @@ +const { request, routes } = require('../lib'); + +const TEST_USER_DATA = { + name: 'TEST_USER', + login: 'test_user', + password: 'T35t_P@55w0rd' +}; + +describe('Users suite', () => { + describe('GET all users', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.users.getAll).expect(401); + }); + }); + + describe('GET user by id', () => { + it('should get 401 without token presented ', async () => { + await request.get(routes.users.getById('123')).expect(401); + }); + }); + + describe('POST', () => { + it('should get 401 without token presented ', async () => { + await request + .post(routes.users.create) + .send(TEST_USER_DATA) + .expect(401); + }); + }); + + describe('PUT', () => { + it('should get 401 without token presented ', async () => { + await request + .put(routes.users.update('12345')) + .send(TEST_USER_DATA) + .expect(401); + }); + }); + + describe('DELETE', () => { + it('should get 401 without token presented ', async () => { + await request.delete(routes.users.delete('12345')).expect(401); + }); + }); +}); diff --git a/backend/test/e2e/test/boards.test.js b/backend/test/e2e/test/boards.test.js new file mode 100644 index 0000000..b2668fb --- /dev/null +++ b/backend/test/e2e/test/boards.test.js @@ -0,0 +1,206 @@ +const { request: unauthorizedRequest, routes } = require('../lib'); +const debug = require('debug')('rs:test:boards'); +const { + createAuthorizedRequest, + shouldAuthorizationBeTested +} = require('../utils'); + +const TEST_BOARD_DATA = { + title: 'Autotest board', + columns: [ + { title: 'Backlog', order: 1 }, + { title: 'Sprint', order: 2 } + ] +}; +describe('Boards suite', () => { + let request = unauthorizedRequest; + let testBoardId; + + beforeAll(async () => { + if (shouldAuthorizationBeTested) { + request = await createAuthorizedRequest(unauthorizedRequest); + } + + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .then(res => (testBoardId = res.body.id)); + }); + + afterAll(async () => { + await request + .delete(routes.boards.delete(testBoardId)) + .then(res => expect(res.status).oneOf([200, 204])); + }); + + describe('GET', () => { + it('should get all boards', async () => { + await request + .get(routes.boards.getAll) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + debug(res.body); + expect(res.body).to.be.an('array'); + jestExpect(res.body).not.toHaveLength(0); + }); + }); + + it('should get a board by id', async () => { + // Setup + let expectedBoard; + + await request + .get(routes.boards.getAll) + .expect(200) + .then(res => { + jestExpect(Array.isArray(res.body)).toBe(true); + jestExpect(res.body).not.toHaveLength(0); + expectedBoard = res.body[0]; + }); + + // Test + await request + .get(routes.boards.getById(expectedBoard.id)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + jestExpect(res.body).toEqual(expectedBoard); + }); + }); + }); + + describe('POST', () => { + it('should create board successfully', async () => { + let boardId; + + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + boardId = res.body.id; + expect(res.body.id).to.be.a('string'); + jestExpect(res.body).toMatchObject(TEST_BOARD_DATA); + }); + + // Teardown + await request.delete(routes.boards.delete(boardId)); + }); + }); + + describe('PUT', () => { + it('should update board successfully', async () => { + // Setup + let boardToUpdate; + + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .then(res => { + boardToUpdate = res.body; + }); + + const updatedBoard = { + ...boardToUpdate, + title: 'Autotest updated board' + }; + + // Test + await request + .put(routes.boards.update(boardToUpdate.id)) + .set('Accept', 'application/json') + .send(updatedBoard) + .expect(200) + .expect('Content-Type', /json/); + + await request + .get(routes.boards.getById(updatedBoard.id)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => jestExpect(res.body).toMatchObject(updatedBoard)); + + // Teardown + await request.delete(routes.boards.delete(updatedBoard.id)); + }); + }); + + describe('DELETE', () => { + it('should delete board successfully', async () => { + // Setup: + let boardId; + + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .expect(200) + .expect('Content-Type', /json/) + .then(res => (boardId = res.body.id)); + + // Test + await request + .delete(routes.boards.delete(boardId)) + .then(res => expect(res.status).oneOf([200, 204])); + + await request.get(routes.boards.getById(boardId)).expect(404); + }); + + it("should delete board's tasks upon deletion", async () => { + // Setup: + const res = await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .expect(200); + const boardId = res.body.id; + + const boardTaskResponses = await Promise.all( + Array.from(Array(5)).map((_, idx) => + request + .post(routes.tasks.create(boardId)) + .send({ + title: `Task #${idx + 1}`, + order: idx + 1, + description: 'Lorem ipsum', + boardId, + userId: null, + columnId: null + }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + ) + ); + + const boardTaskIds = boardTaskResponses.map(response => response.body.id); + await Promise.all( + boardTaskIds.map(async taskId => + request + .get(routes.tasks.getById(boardId, taskId)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(response => expect(response.body.boardId).to.equal(boardId)) + ) + ); + // Test: + await request + .delete(routes.boards.delete(boardId)) + .then(response => expect(response.status).oneOf([200, 204])); + + await Promise.all( + boardTaskIds.map(async taskId => + request.get(routes.tasks.getById(boardId, taskId)).expect(404) + ) + ); + }); + }); +}); diff --git a/backend/test/e2e/test/tasks.test.js b/backend/test/e2e/test/tasks.test.js new file mode 100644 index 0000000..d4482b6 --- /dev/null +++ b/backend/test/e2e/test/tasks.test.js @@ -0,0 +1,166 @@ +const { request: unauthorizedRequest, routes } = require('../lib'); +const debug = require('debug')('rs:test:tasks'); +const { + createAuthorizedRequest, + shouldAuthorizationBeTested +} = require('../utils'); + +const TEST_TASK_DATA = { + title: 'Autotest task', + order: 0, + description: 'Lorem ipsum', + userId: null, + boardId: null, + columnId: null +}; + +const TEST_BOARD_DATA = { + title: 'Autotest board', + columns: [ + { title: 'Backlog', order: 1 }, + { title: 'Sprint', order: 2 } + ] +}; + +describe('Tasks suite', () => { + let request = unauthorizedRequest; + let testTaskId; + let testBoardId; + + beforeAll(async () => { + if (shouldAuthorizationBeTested) { + request = await createAuthorizedRequest(unauthorizedRequest); + } + + await request + .post(routes.boards.create) + .set('Accept', 'application/json') + .send(TEST_BOARD_DATA) + .then(res => (testBoardId = res.body.id)); + + await request + .post(routes.tasks.create(testBoardId)) + .set('Accept', 'application/json') + .send(TEST_TASK_DATA) + .then(res => (testTaskId = res.body.id)); + }); + + afterAll(async () => { + await request + .delete(routes.boards.delete(testBoardId)) + .then(res => expect(res.status).oneOf([200, 204])); + }); + + describe('GET', () => { + it('should get all tasks', async () => { + await request + .get(routes.tasks.getAll(testBoardId)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + debug(res.body); + expect(res.body).to.be.an('array'); + jestExpect(res.body).not.toHaveLength(0); + }); + }); + + it('should get a task by id', async () => { + // Setup + let expectedTask; + + await request + .get(routes.tasks.getAll(testBoardId)) + .expect(200) + .then(res => { + jestExpect(Array.isArray(res.body)).toBe(true); + jestExpect(res.body).not.toHaveLength(0); + expectedTask = res.body[0]; + }); + + // Test + await request + .get(routes.tasks.getById(expectedTask.boardId, expectedTask.id)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + jestExpect(res.body).toEqual(expectedTask); + }); + }); + }); + + describe('POST', () => { + it('should create task successfully', async () => { + let taskId; + + await request + .post(routes.tasks.create(testBoardId)) + .set('Accept', 'application/json') + .send(TEST_TASK_DATA) + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + expect(res.body.id).to.be.a('string'); + taskId = res.body.id; + jestExpect(res.body).toMatchObject({ + ...TEST_TASK_DATA, + boardId: testBoardId + }); + }); + + // Teardown + await request.delete(routes.tasks.delete(testBoardId, taskId)); + }); + }); + + describe('PUT', () => { + it('should update task successfully', async () => { + // Setup + let addedTask; + + await request + .post(routes.tasks.create(testBoardId)) + .set('Accept', 'application/json') + .send(TEST_TASK_DATA) + .then(res => { + addedTask = res.body; + }); + + const updatedTask = { + ...addedTask, + title: 'Autotest updated task' + }; + + // Test + await request + .put(routes.tasks.update(updatedTask.boardId, updatedTask.id)) + .set('Accept', 'application/json') + .send(updatedTask) + .expect(200) + .expect('Content-Type', /json/); + + await request + .get(routes.tasks.getById(updatedTask.boardId, updatedTask.id)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => jestExpect(res.body).toMatchObject(updatedTask)); + }); + }); + + describe('DELETE', () => { + it('should delete task successfully', async () => { + await request + .get(routes.tasks.getById(testBoardId, testTaskId)) + .expect(200); + await request + .delete(routes.tasks.delete(testBoardId, testTaskId)) + .then(res => expect(res.status).oneOf([200, 204])); + + await request + .get(routes.tasks.getById(testBoardId, testTaskId)) + .expect(404); + }); + }); +}); diff --git a/backend/test/e2e/test/users.test.js b/backend/test/e2e/test/users.test.js new file mode 100644 index 0000000..e3a7644 --- /dev/null +++ b/backend/test/e2e/test/users.test.js @@ -0,0 +1,219 @@ +const { request: unauthorizedRequest, routes } = require('../lib'); +const debug = require('debug')('rs:test:users'); +const { + createAuthorizedRequest, + shouldAuthorizationBeTested +} = require('../utils'); + +const TEST_USER_DATA = { + name: 'TEST_USER', + login: 'test_user', + password: 'T35t_P@55w0rd' +}; + +const TEST_BOARD_DATA = { + title: 'Autotest board', + columns: [ + { title: 'Backlog', order: 1 }, + { title: 'Sprint', order: 2 } + ] +}; + +describe('Users suite', () => { + let request = unauthorizedRequest; + + beforeAll(async () => { + if (shouldAuthorizationBeTested) { + request = await createAuthorizedRequest(unauthorizedRequest); + } + }); + + describe('GET', () => { + it('should get all users', async () => { + const usersResponse = await request + .get(routes.users.getAll) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/); + debug(usersResponse.body); + + expect(usersResponse.status).to.equal(200); + expect(Array.isArray(usersResponse.body)).to.be.true(); + }); + + it('should get a user by id', async () => { + // Setup: + let userId; + + // Create the user + await request + .post(routes.users.create) + .set('Accept', 'application/json') + .send(TEST_USER_DATA) + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + expect(res.body.id).to.be.a('string'); + userId = res.body.id; + }); + + // Test: + const userResponse = await request + .get(routes.users.getById(userId)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/); + + expect(userResponse.body).to.be.instanceOf(Object); + expect(userResponse.body.id).to.equal(userId); + + // Clean up, delete the user we created + await request.delete(routes.users.delete(userId)); + }); + }); + + describe('POST', () => { + it('should create user successfully', async () => { + let userId; + + await request + .post(routes.users.create) + .set('Accept', 'application/json') + .send(TEST_USER_DATA) + .expect(200) + .expect('Content-Type', /json/) + .then(res => { + expect(res.body.id).to.be.a('string'); + userId = res.body.id; + expect(res.body).to.not.have.property('password'); + jestExpect(res.body).toMatchObject({ + login: TEST_USER_DATA.login, + name: TEST_USER_DATA.name + }); + }); + + // Teardown + await request.delete(routes.users.delete(userId)); + }); + }); + + describe('PUT', () => { + it('should update user successfully', async () => { + // Setup + let userId; + + await request + .post(routes.users.create) + .set('Accept', 'application/json') + .send(TEST_USER_DATA) + .then(res => { + userId = res.body.id; + }); + + const updatedUser = { + ...TEST_USER_DATA, + name: 'Autotest updated TEST_USER', + id: userId + }; + + // Test + await request + .put(routes.users.update(userId)) + .set('Accept', 'application/json') + .send(updatedUser) + .expect(200) + .expect('Content-Type', /json/); + + // eslint-disable-next-line no-unused-vars + const { password, ...expectedUser } = updatedUser; + + await request + .get(routes.users.getById(userId)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .then(res => jestExpect(res.body).toMatchObject(expectedUser)); + + // Teardown + await request.delete(routes.users.delete(userId)); + }); + }); + + describe('DELETE', () => { + it('should delete user successfully', async () => { + // Setup: + const userResponse = await request + .post(routes.users.create) + .send(TEST_USER_DATA); + const userId = userResponse.body.id; + + // Test: + const deleteResponse = await request.delete(routes.users.delete(userId)); + expect(deleteResponse.status).oneOf([200, 204]); + }); + + it("should unassign user's tasks upon deletion", async () => { + // Setup: + const userResponse = await request + .post(routes.users.create) + .send(TEST_USER_DATA) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/); + const userId = userResponse.body.id; + + const boardResponse = await request + .post(routes.boards.create) + .send(TEST_BOARD_DATA) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/); + const boardId = boardResponse.body.id; + + const userTaskResponses = await Promise.all( + Array.from(Array(2)).map((_, idx) => + request + .post(routes.tasks.create(boardId)) + .send({ + title: `Task #${idx + 1}`, + order: idx + 1, + description: 'Lorem ipsum', + userId, + boardId + }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + ) + ); + const userTaskIds = userTaskResponses.map(res => res.body.id); + + // Test: + const deleteResponse = await request.delete(routes.users.delete(userId)); + expect(deleteResponse.status).oneOf([200, 204]); + + for (const taskId of userTaskIds) { + const newTaskResponse = await request + .get(routes.tasks.getById(boardId, taskId)) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/); + + expect(newTaskResponse.body).to.be.instanceOf(Object); + expect(newTaskResponse.body.userId).to.equal(null); + } + + await Promise.all( + userTaskIds.map(async taskId => + request + .delete(routes.tasks.getById(boardId, taskId)) + .then(response => expect(response.status).oneOf([200, 204])) + ) + ); + + await request + .delete(routes.boards.delete(boardId)) + .then(res => expect(res.status).oneOf([200, 204])); + }); + }); +}); diff --git a/backend/test/e2e/utils/createAuthorizedRequest.js b/backend/test/e2e/utils/createAuthorizedRequest.js new file mode 100644 index 0000000..b807025 --- /dev/null +++ b/backend/test/e2e/utils/createAuthorizedRequest.js @@ -0,0 +1,25 @@ +const { routes } = require('../lib'); + +const createRequestWithToken = (request, token) => { + const obj = {}; + for (const key in request) { + if (Object.prototype.hasOwnProperty.call(request, key)) { + const method = request[key]; + obj[key] = path => method(path).set('Authorization', token); + } + } + + return obj; +}; + +const createAuthorizedRequest = async request => { + const res = await request + .post(routes.login) + .set('Accept', 'application/json') + .send({ login: 'admin', password: 'admin' }); + + const token = `Bearer ${res.body.token}`; + return createRequestWithToken(request, token); +}; + +module.exports = createAuthorizedRequest; diff --git a/backend/test/e2e/utils/index.js b/backend/test/e2e/utils/index.js new file mode 100644 index 0000000..9e73702 --- /dev/null +++ b/backend/test/e2e/utils/index.js @@ -0,0 +1,6 @@ +const createAuthorizedRequest = require('./createAuthorizedRequest'); +const shouldAuthorizationBeTested = require('./shouldAuthorizationBeTested'); +module.exports = { + createAuthorizedRequest, + shouldAuthorizationBeTested +}; diff --git a/backend/test/e2e/utils/shouldAuthorizationBeTested.js b/backend/test/e2e/utils/shouldAuthorizationBeTested.js new file mode 100644 index 0000000..38809c8 --- /dev/null +++ b/backend/test/e2e/utils/shouldAuthorizationBeTested.js @@ -0,0 +1 @@ +module.exports = process.env.TEST_MODE === 'auth'; diff --git a/backend/test/setup.js b/backend/test/setup.js new file mode 100644 index 0000000..f2b5df3 --- /dev/null +++ b/backend/test/setup.js @@ -0,0 +1,7 @@ +const chai = require('chai'); +const dirtyChai = require('dirty-chai'); + +chai.use(dirtyChai); + +global.jestExpect = global.expect; +global.expect = chai.expect; diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..2cfaa49 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,64 @@ +module.exports = { + env: { + browser: true, + es6: true, + }, + extends: [ + // "airbnb-typescript/base" + 'airbnb-base', + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + rules: { + // 'exceptAfterSingleLine': 0, + 'no-console': 'off', + 'no-plusplus': ['error', + { + allowForLoopAfterthoughts: true, + }], + 'no-use-before-define': ['error', + { + functions: false, + }], + 'import/extensions': 'warn', + 'max-len': ['error', { code: 120 }], + }, +}; + +// module.exports = { +// env: { +// browser: true, +// es6: true, +// }, +// extends: ['eslint:recommended', 'airbnb-base', 'plugin:@typescript-eslint/recommended'], +// parser: '@typescript-eslint/parser', +// parserOptions: { +// ecmaVersion: 12, +// sourceType: 'module', +// }, +// plugins: ['@typescript-eslint'], +// rules: { +// 'import/extensions': [ +// 'error', +// 'ignorePackages', +// { +// ts: 'never', +// tsx: 'never', +// }, +// ], +// 'max-len': ['error', { code: 120 }], +// }, +// settings: { +// 'import/resolver': { +// typescript: {}, +// }, +// }, +// }; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9df6242 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,96 @@ +{ + "name": "rsclone", + "version": "1.0.0", + "description": "game with canvas and pure JS (tower defence)", + "keywords": [ + "game", + "phaser", + "kingdom rush", + "tower defence", + "pure JS", + "html5 game", + "TypeScript", + "webpack" + ], + "private": true, + "main": "App.ts", + "scripts": { + "dev": "webpack-dev-server --history-api-fallback --inline --open", + "build": "npm run clear -s && webpack --progress", + "lint": "eslint", + "clear": "del-cli dist" + }, + "engines": { + "node": ">=10.13.0" + }, + "dependencies": { + "@babel/polyfill": "^7.8.3", + "core-js": "^3.4.7", + "phaser": "~3.50.1" + }, + "devDependencies": { + "@babel/core": "^7.8.3", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/preset-env": "^7.8.3", + "@babel/preset-react": "^7.8.3", + "@babel/preset-typescript": "^7.8.3", + "@typescript-eslint/eslint-plugin": "^4.12.0", + "@typescript-eslint/parser": "^4.12.0", + "@webpack-cli/serve": "^1.1.0", + "autoprefixer": "^9.6.1", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.0.6", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^5.1.1", + "cross-env": "^6.0.3", + "css-loader": "^3.4.2", + "css-mqpacker": "^7.0.0", + "cssnano": "^4.1.10", + "csv-loader": "^3.0.2", + "del-cli": "^3.0.0", + "eslint": "^7.12.1", + "eslint-config-airbnb-base": "^14.2.0", + "eslint-config-airbnb-typescript": "^12.0.0", + "eslint-loader": "^3.0.4", + "eslint-plugin-import": "^2.22.1", + "file-loader": "^5.0.2", + "html-loader": "^0.5.5", + "html-webpack-plugin": "^3.2.0", + "image-webpack-loader": "^6.0.0", + "javascript-obfuscator": "^2.9.5", + "mini-css-extract-plugin": "^0.9.0", + "node-sass": "^4.13.0", + "optimize-css-assets-webpack-plugin": "^5.0.3", + "papaparse": "^5.1.1", + "rimraf": "^3.0.2", + "sass-loader": "^8.0.2", + "serve": "^11.3.2", + "style-loader": "^1.1.2", + "terser-webpack-plugin": "^2.3.2", + "ts-loader": "^8.0.12", + "typescript": "^4.1.3", + "uglifyjs-webpack-plugin": "^2.2.0", + "webpack": "^4.41.5", + "webpack-bundle-analyzer": "^3.6.0", + "webpack-cli": "^3.3.10", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^5.7.3", + "webpack-obfuscator": "^3.2.0", + "workbox-webpack-plugin": "^6.0.2", + "xml-loader": "^1.2.1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Iogsotot/rsclone.git" + }, + "author": "Anna Justus ", + "contributes": { + "1": "Uladzimir Kazak ", + "2": "Anna Justus " + }, + "license": "ISC", + "bugs": { + "url": "https://github.com/Iogsotot/rsclone/issues" + }, + "homepage": "https://github.com/Iogsotot/rsclone#readme" +} diff --git a/frontend/src/assets/achievements/achievement_popup.png b/frontend/src/assets/achievements/achievement_popup.png new file mode 100644 index 0000000..2f9998a Binary files /dev/null and b/frontend/src/assets/achievements/achievement_popup.png differ diff --git a/frontend/src/assets/achievements/achievement_popup_2.png b/frontend/src/assets/achievements/achievement_popup_2.png new file mode 100644 index 0000000..791ab55 Binary files /dev/null and b/frontend/src/assets/achievements/achievement_popup_2.png differ diff --git a/frontend/src/assets/achievements/achievement_popup_3.png b/frontend/src/assets/achievements/achievement_popup_3.png new file mode 100644 index 0000000..81ed7db Binary files /dev/null and b/frontend/src/assets/achievements/achievement_popup_3.png differ diff --git a/frontend/src/assets/achievements/achievements_icon.jpg.png b/frontend/src/assets/achievements/achievements_icon.jpg.png new file mode 100644 index 0000000..55a6c61 Binary files /dev/null and b/frontend/src/assets/achievements/achievements_icon.jpg.png differ diff --git a/frontend/src/assets/achievements/achievements_icon.png b/frontend/src/assets/achievements/achievements_icon.png new file mode 100644 index 0000000..653ea0b Binary files /dev/null and b/frontend/src/assets/achievements/achievements_icon.png differ diff --git a/frontend/src/assets/achievements/achievements_icon_2.png b/frontend/src/assets/achievements/achievements_icon_2.png new file mode 100644 index 0000000..2e37eee Binary files /dev/null and b/frontend/src/assets/achievements/achievements_icon_2.png differ diff --git a/frontend/src/assets/achievements/achievements_icon_3.png b/frontend/src/assets/achievements/achievements_icon_3.png new file mode 100644 index 0000000..1de2882 Binary files /dev/null and b/frontend/src/assets/achievements/achievements_icon_3.png differ diff --git a/frontend/src/assets/achievements/builder.png b/frontend/src/assets/achievements/builder.png new file mode 100644 index 0000000..dae34c4 Binary files /dev/null and b/frontend/src/assets/achievements/builder.png differ diff --git a/frontend/src/assets/achievements/complete_win.png b/frontend/src/assets/achievements/complete_win.png new file mode 100644 index 0000000..72accb5 Binary files /dev/null and b/frontend/src/assets/achievements/complete_win.png differ diff --git a/frontend/src/assets/achievements/first_asterisk.png b/frontend/src/assets/achievements/first_asterisk.png new file mode 100644 index 0000000..817e812 Binary files /dev/null and b/frontend/src/assets/achievements/first_asterisk.png differ diff --git a/frontend/src/assets/achievements/first_blood.png b/frontend/src/assets/achievements/first_blood.png new file mode 100644 index 0000000..be72995 Binary files /dev/null and b/frontend/src/assets/achievements/first_blood.png differ diff --git a/frontend/src/assets/achievements/great_defender.png b/frontend/src/assets/achievements/great_defender.png new file mode 100644 index 0000000..67adc1d Binary files /dev/null and b/frontend/src/assets/achievements/great_defender.png differ diff --git a/frontend/src/assets/achievements/iron_defender.png b/frontend/src/assets/achievements/iron_defender.png new file mode 100644 index 0000000..a09c3d0 Binary files /dev/null and b/frontend/src/assets/achievements/iron_defender.png differ diff --git a/frontend/src/assets/achievements/killer.png b/frontend/src/assets/achievements/killer.png new file mode 100644 index 0000000..45d571c Binary files /dev/null and b/frontend/src/assets/achievements/killer.png differ diff --git a/frontend/src/assets/achievements/seller.png b/frontend/src/assets/achievements/seller.png new file mode 100644 index 0000000..7fbb9e1 Binary files /dev/null and b/frontend/src/assets/achievements/seller.png differ diff --git a/frontend/src/assets/achievements/table_3.png b/frontend/src/assets/achievements/table_3.png new file mode 100644 index 0000000..35432f6 Binary files /dev/null and b/frontend/src/assets/achievements/table_3.png differ diff --git a/frontend/src/assets/auth/Asset 10.png b/frontend/src/assets/auth/Asset 10.png new file mode 100644 index 0000000..5f6abe4 Binary files /dev/null and b/frontend/src/assets/auth/Asset 10.png differ diff --git a/frontend/src/assets/auth/Asset 2.png b/frontend/src/assets/auth/Asset 2.png new file mode 100644 index 0000000..06677fa Binary files /dev/null and b/frontend/src/assets/auth/Asset 2.png differ diff --git a/frontend/src/assets/auth/Asset 3.png b/frontend/src/assets/auth/Asset 3.png new file mode 100644 index 0000000..bf48db6 Binary files /dev/null and b/frontend/src/assets/auth/Asset 3.png differ diff --git a/frontend/src/assets/auth/Asset 8.png b/frontend/src/assets/auth/Asset 8.png new file mode 100644 index 0000000..8d67b9d Binary files /dev/null and b/frontend/src/assets/auth/Asset 8.png differ diff --git a/frontend/src/assets/auth/Asset 9.png b/frontend/src/assets/auth/Asset 9.png new file mode 100644 index 0000000..713318a Binary files /dev/null and b/frontend/src/assets/auth/Asset 9.png differ diff --git a/frontend/src/assets/auth/Asset-1.png b/frontend/src/assets/auth/Asset-1.png new file mode 100644 index 0000000..283f371 Binary files /dev/null and b/frontend/src/assets/auth/Asset-1.png differ diff --git a/frontend/src/assets/auth/Game title bg.png b/frontend/src/assets/auth/Game title bg.png new file mode 100644 index 0000000..0debf5e Binary files /dev/null and b/frontend/src/assets/auth/Game title bg.png differ diff --git a/frontend/src/assets/auth/Setting.png b/frontend/src/assets/auth/Setting.png new file mode 100644 index 0000000..7111f92 Binary files /dev/null and b/frontend/src/assets/auth/Setting.png differ diff --git a/frontend/src/assets/auth/UI_StoneFrame_980.png b/frontend/src/assets/auth/UI_StoneFrame_980.png new file mode 100644 index 0000000..f71a684 Binary files /dev/null and b/frontend/src/assets/auth/UI_StoneFrame_980.png differ diff --git a/frontend/src/assets/auth/UI_board_Small_parchment.png b/frontend/src/assets/auth/UI_board_Small_parchment.png new file mode 100644 index 0000000..547cfc7 Binary files /dev/null and b/frontend/src/assets/auth/UI_board_Small_parchment.png differ diff --git a/frontend/src/assets/auth/achievement without achiev text.png b/frontend/src/assets/auth/achievement without achiev text.png new file mode 100644 index 0000000..ad4a771 Binary files /dev/null and b/frontend/src/assets/auth/achievement without achiev text.png differ diff --git a/frontend/src/assets/auth/achievement.png b/frontend/src/assets/auth/achievement.png new file mode 100644 index 0000000..bb072c7 Binary files /dev/null and b/frontend/src/assets/auth/achievement.png differ diff --git a/frontend/src/assets/auth/achievement_board.png b/frontend/src/assets/auth/achievement_board.png new file mode 100644 index 0000000..2f9998a Binary files /dev/null and b/frontend/src/assets/auth/achievement_board.png differ diff --git a/frontend/src/assets/auth/achievements-btn.png b/frontend/src/assets/auth/achievements-btn.png new file mode 100644 index 0000000..e813056 Binary files /dev/null and b/frontend/src/assets/auth/achievements-btn.png differ diff --git a/frontend/src/assets/auth/any btn.png b/frontend/src/assets/auth/any btn.png new file mode 100644 index 0000000..c26b475 Binary files /dev/null and b/frontend/src/assets/auth/any btn.png differ diff --git a/frontend/src/assets/auth/bag-attendance.png b/frontend/src/assets/auth/bag-attendance.png new file mode 100644 index 0000000..3097924 Binary files /dev/null and b/frontend/src/assets/auth/bag-attendance.png differ diff --git a/frontend/src/assets/auth/btn.png b/frontend/src/assets/auth/btn.png new file mode 100644 index 0000000..308150c Binary files /dev/null and b/frontend/src/assets/auth/btn.png differ diff --git a/frontend/src/assets/auth/bubble 1.png b/frontend/src/assets/auth/bubble 1.png new file mode 100644 index 0000000..f6e34fb Binary files /dev/null and b/frontend/src/assets/auth/bubble 1.png differ diff --git a/frontend/src/assets/auth/bubble 2.png b/frontend/src/assets/auth/bubble 2.png new file mode 100644 index 0000000..566e64c Binary files /dev/null and b/frontend/src/assets/auth/bubble 2.png differ diff --git a/frontend/src/assets/auth/bubble 3.png b/frontend/src/assets/auth/bubble 3.png new file mode 100644 index 0000000..cb4f302 Binary files /dev/null and b/frontend/src/assets/auth/bubble 3.png differ diff --git a/frontend/src/assets/auth/bubble 4.png b/frontend/src/assets/auth/bubble 4.png new file mode 100644 index 0000000..3e33c62 Binary files /dev/null and b/frontend/src/assets/auth/bubble 4.png differ diff --git a/frontend/src/assets/auth/close.png b/frontend/src/assets/auth/close.png new file mode 100644 index 0000000..8d533c5 Binary files /dev/null and b/frontend/src/assets/auth/close.png differ diff --git a/frontend/src/assets/auth/close2.png b/frontend/src/assets/auth/close2.png new file mode 100644 index 0000000..5a5a244 Binary files /dev/null and b/frontend/src/assets/auth/close2.png differ diff --git a/frontend/src/assets/auth/difficuls seelction.png b/frontend/src/assets/auth/difficuls seelction.png new file mode 100644 index 0000000..718fb77 Binary files /dev/null and b/frontend/src/assets/auth/difficuls seelction.png differ diff --git a/frontend/src/assets/auth/easy.png b/frontend/src/assets/auth/easy.png new file mode 100644 index 0000000..ef0b8b6 Binary files /dev/null and b/frontend/src/assets/auth/easy.png differ diff --git a/frontend/src/assets/auth/en.png b/frontend/src/assets/auth/en.png new file mode 100644 index 0000000..c029aa7 Binary files /dev/null and b/frontend/src/assets/auth/en.png differ diff --git a/frontend/src/assets/auth/field.png b/frontend/src/assets/auth/field.png new file mode 100644 index 0000000..86ae2f8 Binary files /dev/null and b/frontend/src/assets/auth/field.png differ diff --git a/frontend/src/assets/auth/hard.png b/frontend/src/assets/auth/hard.png new file mode 100644 index 0000000..c45c8c0 Binary files /dev/null and b/frontend/src/assets/auth/hard.png differ diff --git a/frontend/src/assets/auth/interface.png b/frontend/src/assets/auth/interface.png new file mode 100644 index 0000000..2173ce1 Binary files /dev/null and b/frontend/src/assets/auth/interface.png differ diff --git a/frontend/src/assets/auth/interface_start-game (1).png b/frontend/src/assets/auth/interface_start-game (1).png new file mode 100644 index 0000000..5eb5e6b Binary files /dev/null and b/frontend/src/assets/auth/interface_start-game (1).png differ diff --git a/frontend/src/assets/auth/kingdom-rush.png b/frontend/src/assets/auth/kingdom-rush.png new file mode 100644 index 0000000..a5d1638 Binary files /dev/null and b/frontend/src/assets/auth/kingdom-rush.png differ diff --git a/frontend/src/assets/auth/logo.png b/frontend/src/assets/auth/logo.png new file mode 100644 index 0000000..339c333 Binary files /dev/null and b/frontend/src/assets/auth/logo.png differ diff --git a/frontend/src/assets/auth/menu btn.png b/frontend/src/assets/auth/menu btn.png new file mode 100644 index 0000000..a681579 Binary files /dev/null and b/frontend/src/assets/auth/menu btn.png differ diff --git a/frontend/src/assets/auth/minus.png b/frontend/src/assets/auth/minus.png new file mode 100644 index 0000000..61e4893 Binary files /dev/null and b/frontend/src/assets/auth/minus.png differ diff --git a/frontend/src/assets/auth/modal board.png b/frontend/src/assets/auth/modal board.png new file mode 100644 index 0000000..669051c Binary files /dev/null and b/frontend/src/assets/auth/modal board.png differ diff --git a/frontend/src/assets/auth/modal text field.png b/frontend/src/assets/auth/modal text field.png new file mode 100644 index 0000000..df5f94d Binary files /dev/null and b/frontend/src/assets/auth/modal text field.png differ diff --git a/frontend/src/assets/auth/modal.png b/frontend/src/assets/auth/modal.png new file mode 100644 index 0000000..bc68718 Binary files /dev/null and b/frontend/src/assets/auth/modal.png differ diff --git a/frontend/src/assets/auth/music_text.png b/frontend/src/assets/auth/music_text.png new file mode 100644 index 0000000..92fb811 Binary files /dev/null and b/frontend/src/assets/auth/music_text.png differ diff --git a/frontend/src/assets/auth/next btn.png b/frontend/src/assets/auth/next btn.png new file mode 100644 index 0000000..817b4fa Binary files /dev/null and b/frontend/src/assets/auth/next btn.png differ diff --git a/frontend/src/assets/auth/next wave icon with timer.png b/frontend/src/assets/auth/next wave icon with timer.png new file mode 100644 index 0000000..0d3e7d6 Binary files /dev/null and b/frontend/src/assets/auth/next wave icon with timer.png differ diff --git a/frontend/src/assets/auth/next wave icon.png b/frontend/src/assets/auth/next wave icon.png new file mode 100644 index 0000000..3c82479 Binary files /dev/null and b/frontend/src/assets/auth/next wave icon.png differ diff --git a/frontend/src/assets/auth/normal.png b/frontend/src/assets/auth/normal.png new file mode 100644 index 0000000..474f62d Binary files /dev/null and b/frontend/src/assets/auth/normal.png differ diff --git a/frontend/src/assets/auth/off.png b/frontend/src/assets/auth/off.png new file mode 100644 index 0000000..584a379 Binary files /dev/null and b/frontend/src/assets/auth/off.png differ diff --git a/frontend/src/assets/auth/on.png b/frontend/src/assets/auth/on.png new file mode 100644 index 0000000..06efde0 Binary files /dev/null and b/frontend/src/assets/auth/on.png differ diff --git a/frontend/src/assets/auth/plus.png b/frontend/src/assets/auth/plus.png new file mode 100644 index 0000000..b2a945a Binary files /dev/null and b/frontend/src/assets/auth/plus.png differ diff --git a/frontend/src/assets/auth/prev btn.png b/frontend/src/assets/auth/prev btn.png new file mode 100644 index 0000000..57b299b Binary files /dev/null and b/frontend/src/assets/auth/prev btn.png differ diff --git a/frontend/src/assets/auth/progress-bar.png b/frontend/src/assets/auth/progress-bar.png new file mode 100644 index 0000000..73982bf Binary files /dev/null and b/frontend/src/assets/auth/progress-bar.png differ diff --git a/frontend/src/assets/auth/ru.png b/frontend/src/assets/auth/ru.png new file mode 100644 index 0000000..69f32bb Binary files /dev/null and b/frontend/src/assets/auth/ru.png differ diff --git a/frontend/src/assets/auth/text btn.png b/frontend/src/assets/auth/text btn.png new file mode 100644 index 0000000..552d7d9 Binary files /dev/null and b/frontend/src/assets/auth/text btn.png differ diff --git a/frontend/src/assets/auth/title.png b/frontend/src/assets/auth/title.png new file mode 100644 index 0000000..88bc646 Binary files /dev/null and b/frontend/src/assets/auth/title.png differ diff --git a/frontend/src/assets/auth/uz.png b/frontend/src/assets/auth/uz.png new file mode 100644 index 0000000..3d7cfee Binary files /dev/null and b/frontend/src/assets/auth/uz.png differ diff --git a/frontend/src/assets/credits/paper_3.png b/frontend/src/assets/credits/paper_3.png new file mode 100644 index 0000000..64c9c49 Binary files /dev/null and b/frontend/src/assets/credits/paper_3.png differ diff --git a/frontend/src/assets/credits/wood_2.png b/frontend/src/assets/credits/wood_2.png new file mode 100644 index 0000000..f87941d Binary files /dev/null and b/frontend/src/assets/credits/wood_2.png differ diff --git a/frontend/src/assets/fonts/Dimbo-ru-en.otf b/frontend/src/assets/fonts/Dimbo-ru-en.otf new file mode 100644 index 0000000..c394e54 Binary files /dev/null and b/frontend/src/assets/fonts/Dimbo-ru-en.otf differ diff --git a/frontend/src/assets/icons/armor.png b/frontend/src/assets/icons/armor.png new file mode 100644 index 0000000..3675cb9 Binary files /dev/null and b/frontend/src/assets/icons/armor.png differ diff --git a/frontend/src/assets/icons/arrows.png b/frontend/src/assets/icons/arrows.png new file mode 100644 index 0000000..408ab7b Binary files /dev/null and b/frontend/src/assets/icons/arrows.png differ diff --git a/frontend/src/assets/icons/bomb.png b/frontend/src/assets/icons/bomb.png new file mode 100644 index 0000000..7b4e45a Binary files /dev/null and b/frontend/src/assets/icons/bomb.png differ diff --git a/frontend/src/assets/icons/coins.png b/frontend/src/assets/icons/coins.png new file mode 100644 index 0000000..4bdc200 Binary files /dev/null and b/frontend/src/assets/icons/coins.png differ diff --git a/frontend/src/assets/icons/damage.png b/frontend/src/assets/icons/damage.png new file mode 100644 index 0000000..cf5442c Binary files /dev/null and b/frontend/src/assets/icons/damage.png differ diff --git a/frontend/src/assets/icons/heart.png b/frontend/src/assets/icons/heart.png new file mode 100644 index 0000000..e085d35 Binary files /dev/null and b/frontend/src/assets/icons/heart.png differ diff --git a/frontend/src/assets/icons/hour-glass.png b/frontend/src/assets/icons/hour-glass.png new file mode 100644 index 0000000..17d673a Binary files /dev/null and b/frontend/src/assets/icons/hour-glass.png differ diff --git a/frontend/src/assets/icons/magic.png b/frontend/src/assets/icons/magic.png new file mode 100644 index 0000000..3c58b55 Binary files /dev/null and b/frontend/src/assets/icons/magic.png differ diff --git a/frontend/src/assets/icons/rs_school_js.svg b/frontend/src/assets/icons/rs_school_js.svg new file mode 100644 index 0000000..0a0aa03 --- /dev/null +++ b/frontend/src/assets/icons/rs_school_js.svg @@ -0,0 +1 @@ +rs_school_js \ No newline at end of file diff --git a/frontend/src/assets/icons/shoes.png b/frontend/src/assets/icons/shoes.png new file mode 100644 index 0000000..6235a54 Binary files /dev/null and b/frontend/src/assets/icons/shoes.png differ diff --git a/frontend/src/assets/icons/shoes1.png b/frontend/src/assets/icons/shoes1.png new file mode 100644 index 0000000..711fea3 Binary files /dev/null and b/frontend/src/assets/icons/shoes1.png differ diff --git a/frontend/src/assets/icons/skull.png b/frontend/src/assets/icons/skull.png new file mode 100644 index 0000000..822d3f3 Binary files /dev/null and b/frontend/src/assets/icons/skull.png differ diff --git a/frontend/src/assets/icons/speed2.png b/frontend/src/assets/icons/speed2.png new file mode 100644 index 0000000..928f682 Binary files /dev/null and b/frontend/src/assets/icons/speed2.png differ diff --git a/frontend/src/assets/icons/target.png b/frontend/src/assets/icons/target.png new file mode 100644 index 0000000..e8c4233 Binary files /dev/null and b/frontend/src/assets/icons/target.png differ diff --git a/frontend/src/assets/icons/wave_button.png b/frontend/src/assets/icons/wave_button.png new file mode 100644 index 0000000..8cc79fe Binary files /dev/null and b/frontend/src/assets/icons/wave_button.png differ diff --git a/frontend/src/assets/imgs/desert_scene3.jpg b/frontend/src/assets/imgs/desert_scene3.jpg new file mode 100644 index 0000000..0270801 Binary files /dev/null and b/frontend/src/assets/imgs/desert_scene3.jpg differ diff --git a/frontend/src/assets/imgs/forest_scene1.jpg b/frontend/src/assets/imgs/forest_scene1.jpg new file mode 100644 index 0000000..02834f0 Binary files /dev/null and b/frontend/src/assets/imgs/forest_scene1.jpg differ diff --git a/frontend/src/assets/imgs/forest_scene2.jpg b/frontend/src/assets/imgs/forest_scene2.jpg new file mode 100644 index 0000000..0a86318 Binary files /dev/null and b/frontend/src/assets/imgs/forest_scene2.jpg differ diff --git a/frontend/src/assets/imgs/gate-mini.png b/frontend/src/assets/imgs/gate-mini.png new file mode 100644 index 0000000..a93b82d Binary files /dev/null and b/frontend/src/assets/imgs/gate-mini.png differ diff --git a/frontend/src/assets/imgs/gate.png b/frontend/src/assets/imgs/gate.png new file mode 100644 index 0000000..9c8af3e Binary files /dev/null and b/frontend/src/assets/imgs/gate.png differ diff --git a/frontend/src/assets/interface/btn-pressed.png b/frontend/src/assets/interface/btn-pressed.png new file mode 100644 index 0000000..35a1544 Binary files /dev/null and b/frontend/src/assets/interface/btn-pressed.png differ diff --git a/frontend/src/assets/interface/btn.png b/frontend/src/assets/interface/btn.png new file mode 100644 index 0000000..7a8b0fd Binary files /dev/null and b/frontend/src/assets/interface/btn.png differ diff --git a/frontend/src/assets/interface/button_close.png b/frontend/src/assets/interface/button_close.png new file mode 100644 index 0000000..0516f20 Binary files /dev/null and b/frontend/src/assets/interface/button_close.png differ diff --git a/frontend/src/assets/interface/button_left.png b/frontend/src/assets/interface/button_left.png new file mode 100644 index 0000000..fe6edf6 Binary files /dev/null and b/frontend/src/assets/interface/button_left.png differ diff --git a/frontend/src/assets/interface/button_menu.png b/frontend/src/assets/interface/button_menu.png new file mode 100644 index 0000000..6020f39 Binary files /dev/null and b/frontend/src/assets/interface/button_menu.png differ diff --git a/frontend/src/assets/interface/button_minus.png b/frontend/src/assets/interface/button_minus.png new file mode 100644 index 0000000..3c482fb Binary files /dev/null and b/frontend/src/assets/interface/button_minus.png differ diff --git a/frontend/src/assets/interface/button_pause.png b/frontend/src/assets/interface/button_pause.png new file mode 100644 index 0000000..d25af15 Binary files /dev/null and b/frontend/src/assets/interface/button_pause.png differ diff --git a/frontend/src/assets/interface/button_plus.png b/frontend/src/assets/interface/button_plus.png new file mode 100644 index 0000000..c4396c2 Binary files /dev/null and b/frontend/src/assets/interface/button_plus.png differ diff --git a/frontend/src/assets/interface/button_quick.png b/frontend/src/assets/interface/button_quick.png new file mode 100644 index 0000000..3d3e305 Binary files /dev/null and b/frontend/src/assets/interface/button_quick.png differ diff --git a/frontend/src/assets/interface/button_restart.png b/frontend/src/assets/interface/button_restart.png new file mode 100644 index 0000000..90be525 Binary files /dev/null and b/frontend/src/assets/interface/button_restart.png differ diff --git a/frontend/src/assets/interface/button_right.png b/frontend/src/assets/interface/button_right.png new file mode 100644 index 0000000..a986d0e Binary files /dev/null and b/frontend/src/assets/interface/button_right.png differ diff --git a/frontend/src/assets/interface/button_start.png b/frontend/src/assets/interface/button_start.png new file mode 100644 index 0000000..c05b5a7 Binary files /dev/null and b/frontend/src/assets/interface/button_start.png differ diff --git a/frontend/src/assets/interface/close-btn.png b/frontend/src/assets/interface/close-btn.png new file mode 100644 index 0000000..7c02eee Binary files /dev/null and b/frontend/src/assets/interface/close-btn.png differ diff --git a/frontend/src/assets/interface/easy-btn-bg.png b/frontend/src/assets/interface/easy-btn-bg.png new file mode 100644 index 0000000..0c5bfaf Binary files /dev/null and b/frontend/src/assets/interface/easy-btn-bg.png differ diff --git a/frontend/src/assets/interface/easy_btn.png b/frontend/src/assets/interface/easy_btn.png new file mode 100644 index 0000000..47c3ef7 Binary files /dev/null and b/frontend/src/assets/interface/easy_btn.png differ diff --git a/frontend/src/assets/interface/hard-btn-bg.png b/frontend/src/assets/interface/hard-btn-bg.png new file mode 100644 index 0000000..39fd45c Binary files /dev/null and b/frontend/src/assets/interface/hard-btn-bg.png differ diff --git a/frontend/src/assets/interface/hard_btn.png b/frontend/src/assets/interface/hard_btn.png new file mode 100644 index 0000000..e8d4e67 Binary files /dev/null and b/frontend/src/assets/interface/hard_btn.png differ diff --git a/frontend/src/assets/interface/help.png b/frontend/src/assets/interface/help.png new file mode 100644 index 0000000..23d3b86 Binary files /dev/null and b/frontend/src/assets/interface/help.png differ diff --git a/frontend/src/assets/interface/icon_level_1.png b/frontend/src/assets/interface/icon_level_1.png new file mode 100644 index 0000000..19be2a6 Binary files /dev/null and b/frontend/src/assets/interface/icon_level_1.png differ diff --git a/frontend/src/assets/interface/icon_level_2.png b/frontend/src/assets/interface/icon_level_2.png new file mode 100644 index 0000000..158fb5d Binary files /dev/null and b/frontend/src/assets/interface/icon_level_2.png differ diff --git a/frontend/src/assets/interface/icon_level_3.png b/frontend/src/assets/interface/icon_level_3.png new file mode 100644 index 0000000..8e4cec0 Binary files /dev/null and b/frontend/src/assets/interface/icon_level_3.png differ diff --git a/frontend/src/assets/interface/minus.png b/frontend/src/assets/interface/minus.png new file mode 100644 index 0000000..61e4893 Binary files /dev/null and b/frontend/src/assets/interface/minus.png differ diff --git a/frontend/src/assets/interface/modal-bg.png b/frontend/src/assets/interface/modal-bg.png new file mode 100644 index 0000000..13f6edf Binary files /dev/null and b/frontend/src/assets/interface/modal-bg.png differ diff --git a/frontend/src/assets/interface/music_text.png b/frontend/src/assets/interface/music_text.png new file mode 100644 index 0000000..92fb811 Binary files /dev/null and b/frontend/src/assets/interface/music_text.png differ diff --git a/frontend/src/assets/interface/normal-btn-bg.png b/frontend/src/assets/interface/normal-btn-bg.png new file mode 100644 index 0000000..44f1005 Binary files /dev/null and b/frontend/src/assets/interface/normal-btn-bg.png differ diff --git a/frontend/src/assets/interface/normal_btn.png b/frontend/src/assets/interface/normal_btn.png new file mode 100644 index 0000000..7b17f2a Binary files /dev/null and b/frontend/src/assets/interface/normal_btn.png differ diff --git a/frontend/src/assets/interface/off.png b/frontend/src/assets/interface/off.png new file mode 100644 index 0000000..584a379 Binary files /dev/null and b/frontend/src/assets/interface/off.png differ diff --git a/frontend/src/assets/interface/on.png b/frontend/src/assets/interface/on.png new file mode 100644 index 0000000..06efde0 Binary files /dev/null and b/frontend/src/assets/interface/on.png differ diff --git a/frontend/src/assets/interface/plus.png b/frontend/src/assets/interface/plus.png new file mode 100644 index 0000000..b2a945a Binary files /dev/null and b/frontend/src/assets/interface/plus.png differ diff --git a/frontend/src/assets/interface/rope_big.png b/frontend/src/assets/interface/rope_big.png new file mode 100644 index 0000000..173e963 Binary files /dev/null and b/frontend/src/assets/interface/rope_big.png differ diff --git a/frontend/src/assets/interface/rope_small.png b/frontend/src/assets/interface/rope_small.png new file mode 100644 index 0000000..5cb8acf Binary files /dev/null and b/frontend/src/assets/interface/rope_small.png differ diff --git a/frontend/src/assets/interface/settings-icon.png b/frontend/src/assets/interface/settings-icon.png new file mode 100644 index 0000000..5ad7ede Binary files /dev/null and b/frontend/src/assets/interface/settings-icon.png differ diff --git a/frontend/src/assets/interface/slider-bar-bg.png b/frontend/src/assets/interface/slider-bar-bg.png new file mode 100644 index 0000000..6aed85f Binary files /dev/null and b/frontend/src/assets/interface/slider-bar-bg.png differ diff --git a/frontend/src/assets/interface/sound_text.png b/frontend/src/assets/interface/sound_text.png new file mode 100644 index 0000000..6d752ab Binary files /dev/null and b/frontend/src/assets/interface/sound_text.png differ diff --git a/frontend/src/assets/interface/star-0.png b/frontend/src/assets/interface/star-0.png new file mode 100644 index 0000000..4aaa033 Binary files /dev/null and b/frontend/src/assets/interface/star-0.png differ diff --git a/frontend/src/assets/interface/star-1.png b/frontend/src/assets/interface/star-1.png new file mode 100644 index 0000000..16f36d2 Binary files /dev/null and b/frontend/src/assets/interface/star-1.png differ diff --git a/frontend/src/assets/interface/star-2.png b/frontend/src/assets/interface/star-2.png new file mode 100644 index 0000000..4d10b28 Binary files /dev/null and b/frontend/src/assets/interface/star-2.png differ diff --git a/frontend/src/assets/interface/star-3.png b/frontend/src/assets/interface/star-3.png new file mode 100644 index 0000000..02216ff Binary files /dev/null and b/frontend/src/assets/interface/star-3.png differ diff --git a/frontend/src/assets/interface/star-grey.png b/frontend/src/assets/interface/star-grey.png new file mode 100644 index 0000000..5d28021 Binary files /dev/null and b/frontend/src/assets/interface/star-grey.png differ diff --git a/frontend/src/assets/interface/title-bg.png b/frontend/src/assets/interface/title-bg.png new file mode 100644 index 0000000..8e4f865 Binary files /dev/null and b/frontend/src/assets/interface/title-bg.png differ diff --git a/frontend/src/assets/level_1_title.png b/frontend/src/assets/level_1_title.png new file mode 100644 index 0000000..58331fe Binary files /dev/null and b/frontend/src/assets/level_1_title.png differ diff --git a/frontend/src/assets/level_1_title_mini.png b/frontend/src/assets/level_1_title_mini.png new file mode 100644 index 0000000..a6d8837 Binary files /dev/null and b/frontend/src/assets/level_1_title_mini.png differ diff --git a/frontend/src/assets/level_2_title.png b/frontend/src/assets/level_2_title.png new file mode 100644 index 0000000..f6fff63 Binary files /dev/null and b/frontend/src/assets/level_2_title.png differ diff --git a/frontend/src/assets/level_2_title_mini.png b/frontend/src/assets/level_2_title_mini.png new file mode 100644 index 0000000..7c6cb05 Binary files /dev/null and b/frontend/src/assets/level_2_title_mini.png differ diff --git a/frontend/src/assets/level_3_title.png b/frontend/src/assets/level_3_title.png new file mode 100644 index 0000000..c00d32c Binary files /dev/null and b/frontend/src/assets/level_3_title.png differ diff --git a/frontend/src/assets/level_3_title_mini.png b/frontend/src/assets/level_3_title_mini.png new file mode 100644 index 0000000..57abcd2 Binary files /dev/null and b/frontend/src/assets/level_3_title_mini.png differ diff --git a/frontend/src/assets/main-bg.jpg b/frontend/src/assets/main-bg.jpg new file mode 100644 index 0000000..929ccea Binary files /dev/null and b/frontend/src/assets/main-bg.jpg differ diff --git a/frontend/src/assets/modal-bg/audio-set-bg.png b/frontend/src/assets/modal-bg/audio-set-bg.png new file mode 100644 index 0000000..3f442d7 Binary files /dev/null and b/frontend/src/assets/modal-bg/audio-set-bg.png differ diff --git a/frontend/src/assets/modal-bg/fail-bg.png b/frontend/src/assets/modal-bg/fail-bg.png new file mode 100644 index 0000000..0b2fcd0 Binary files /dev/null and b/frontend/src/assets/modal-bg/fail-bg.png differ diff --git a/frontend/src/assets/modal-bg/failed-modal-bg.png b/frontend/src/assets/modal-bg/failed-modal-bg.png new file mode 100644 index 0000000..3354fc8 Binary files /dev/null and b/frontend/src/assets/modal-bg/failed-modal-bg.png differ diff --git a/frontend/src/assets/modal-bg/hotkeys-modal.png b/frontend/src/assets/modal-bg/hotkeys-modal.png new file mode 100644 index 0000000..22de12e Binary files /dev/null and b/frontend/src/assets/modal-bg/hotkeys-modal.png differ diff --git a/frontend/src/assets/modal-bg/settings-modal-bg.png b/frontend/src/assets/modal-bg/settings-modal-bg.png new file mode 100644 index 0000000..4cdc6d2 Binary files /dev/null and b/frontend/src/assets/modal-bg/settings-modal-bg.png differ diff --git a/frontend/src/assets/modal-bg/start-modal-bg.png b/frontend/src/assets/modal-bg/start-modal-bg.png new file mode 100644 index 0000000..09b441b Binary files /dev/null and b/frontend/src/assets/modal-bg/start-modal-bg.png differ diff --git a/frontend/src/assets/modal-bg/table.png b/frontend/src/assets/modal-bg/table.png new file mode 100644 index 0000000..d269240 Binary files /dev/null and b/frontend/src/assets/modal-bg/table.png differ diff --git a/frontend/src/assets/modal-bg/win-bg.png b/frontend/src/assets/modal-bg/win-bg.png new file mode 100644 index 0000000..40e7614 Binary files /dev/null and b/frontend/src/assets/modal-bg/win-bg.png differ diff --git a/frontend/src/assets/modal-bg/win-modal-bg.png b/frontend/src/assets/modal-bg/win-modal-bg.png new file mode 100644 index 0000000..1851b5d Binary files /dev/null and b/frontend/src/assets/modal-bg/win-modal-bg.png differ diff --git a/frontend/src/assets/modal-headers/header.png b/frontend/src/assets/modal-headers/header.png new file mode 100644 index 0000000..ce29150 Binary files /dev/null and b/frontend/src/assets/modal-headers/header.png differ diff --git a/frontend/src/assets/modal-headers/header_achievement.png b/frontend/src/assets/modal-headers/header_achievement.png new file mode 100644 index 0000000..df52076 Binary files /dev/null and b/frontend/src/assets/modal-headers/header_achievement.png differ diff --git a/frontend/src/assets/modal-headers/header_diff.png b/frontend/src/assets/modal-headers/header_diff.png new file mode 100644 index 0000000..0fb146f Binary files /dev/null and b/frontend/src/assets/modal-headers/header_diff.png differ diff --git a/frontend/src/assets/modal-headers/header_failed.png b/frontend/src/assets/modal-headers/header_failed.png new file mode 100644 index 0000000..a68d650 Binary files /dev/null and b/frontend/src/assets/modal-headers/header_failed.png differ diff --git a/frontend/src/assets/modal-headers/header_settings.png b/frontend/src/assets/modal-headers/header_settings.png new file mode 100644 index 0000000..302e7d5 Binary files /dev/null and b/frontend/src/assets/modal-headers/header_settings.png differ diff --git a/frontend/src/assets/modal-headers/header_win.png b/frontend/src/assets/modal-headers/header_win.png new file mode 100644 index 0000000..e15117a Binary files /dev/null and b/frontend/src/assets/modal-headers/header_win.png differ diff --git a/frontend/src/assets/modal-headers/level1-header.png b/frontend/src/assets/modal-headers/level1-header.png new file mode 100644 index 0000000..c0da3a9 Binary files /dev/null and b/frontend/src/assets/modal-headers/level1-header.png differ diff --git a/frontend/src/assets/modal-headers/level10-header.png b/frontend/src/assets/modal-headers/level10-header.png new file mode 100644 index 0000000..d368e87 Binary files /dev/null and b/frontend/src/assets/modal-headers/level10-header.png differ diff --git a/frontend/src/assets/modal-headers/level2-header.png b/frontend/src/assets/modal-headers/level2-header.png new file mode 100644 index 0000000..f534015 Binary files /dev/null and b/frontend/src/assets/modal-headers/level2-header.png differ diff --git a/frontend/src/assets/modal-headers/level3-header.png b/frontend/src/assets/modal-headers/level3-header.png new file mode 100644 index 0000000..1fd5b5c Binary files /dev/null and b/frontend/src/assets/modal-headers/level3-header.png differ diff --git a/frontend/src/assets/modal-headers/level4-header.png b/frontend/src/assets/modal-headers/level4-header.png new file mode 100644 index 0000000..49571c5 Binary files /dev/null and b/frontend/src/assets/modal-headers/level4-header.png differ diff --git a/frontend/src/assets/modal-headers/level5-header.png b/frontend/src/assets/modal-headers/level5-header.png new file mode 100644 index 0000000..ff48d4f Binary files /dev/null and b/frontend/src/assets/modal-headers/level5-header.png differ diff --git a/frontend/src/assets/modal-headers/level6-header.png b/frontend/src/assets/modal-headers/level6-header.png new file mode 100644 index 0000000..bd54136 Binary files /dev/null and b/frontend/src/assets/modal-headers/level6-header.png differ diff --git a/frontend/src/assets/modal-headers/level7-header.png b/frontend/src/assets/modal-headers/level7-header.png new file mode 100644 index 0000000..96fbdd8 Binary files /dev/null and b/frontend/src/assets/modal-headers/level7-header.png differ diff --git a/frontend/src/assets/modal-headers/level8-header.png b/frontend/src/assets/modal-headers/level8-header.png new file mode 100644 index 0000000..e3f79cc Binary files /dev/null and b/frontend/src/assets/modal-headers/level8-header.png differ diff --git a/frontend/src/assets/modal-headers/level9-header.png b/frontend/src/assets/modal-headers/level9-header.png new file mode 100644 index 0000000..c3028c0 Binary files /dev/null and b/frontend/src/assets/modal-headers/level9-header.png differ diff --git a/frontend/src/assets/overlay_60.png b/frontend/src/assets/overlay_60.png new file mode 100644 index 0000000..c06ee1b Binary files /dev/null and b/frontend/src/assets/overlay_60.png differ diff --git a/frontend/src/assets/sign/offNoText.png b/frontend/src/assets/sign/offNoText.png new file mode 100644 index 0000000..448299b Binary files /dev/null and b/frontend/src/assets/sign/offNoText.png differ diff --git a/frontend/src/assets/sign/onNoText.png b/frontend/src/assets/sign/onNoText.png new file mode 100644 index 0000000..9191928 Binary files /dev/null and b/frontend/src/assets/sign/onNoText.png differ diff --git a/frontend/src/assets/sounds/Coins.wav b/frontend/src/assets/sounds/Coins.wav new file mode 100644 index 0000000..eb9c4d2 Binary files /dev/null and b/frontend/src/assets/sounds/Coins.wav differ diff --git a/frontend/src/assets/sounds/GUI.mp3 b/frontend/src/assets/sounds/GUI.mp3 new file mode 100644 index 0000000..688a473 Binary files /dev/null and b/frontend/src/assets/sounds/GUI.mp3 differ diff --git a/frontend/src/assets/sounds/GUI_ButtonCommon.wav b/frontend/src/assets/sounds/GUI_ButtonCommon.wav new file mode 100644 index 0000000..8acb705 Binary files /dev/null and b/frontend/src/assets/sounds/GUI_ButtonCommon.wav differ diff --git a/frontend/src/assets/sounds/GUI_MouseOverTowerIcon.wav b/frontend/src/assets/sounds/GUI_MouseOverTowerIcon.wav new file mode 100644 index 0000000..ed484a1 Binary files /dev/null and b/frontend/src/assets/sounds/GUI_MouseOverTowerIcon.wav differ diff --git a/frontend/src/assets/sounds/GUI_button.mp3 b/frontend/src/assets/sounds/GUI_button.mp3 new file mode 100644 index 0000000..c602b64 Binary files /dev/null and b/frontend/src/assets/sounds/GUI_button.mp3 differ diff --git a/frontend/src/assets/sounds/GUI_logout.mp3 b/frontend/src/assets/sounds/GUI_logout.mp3 new file mode 100644 index 0000000..d897a53 Binary files /dev/null and b/frontend/src/assets/sounds/GUI_logout.mp3 differ diff --git a/frontend/src/assets/sounds/GUI_pick(quet).mp3 b/frontend/src/assets/sounds/GUI_pick(quet).mp3 new file mode 100644 index 0000000..eb24eff Binary files /dev/null and b/frontend/src/assets/sounds/GUI_pick(quet).mp3 differ diff --git a/frontend/src/assets/sounds/GUI_pick.mp3 b/frontend/src/assets/sounds/GUI_pick.mp3 new file mode 100644 index 0000000..6ab88eb Binary files /dev/null and b/frontend/src/assets/sounds/GUI_pick.mp3 differ diff --git a/frontend/src/assets/sounds/achievement_unlock.wav b/frontend/src/assets/sounds/achievement_unlock.wav new file mode 100644 index 0000000..c4b2fdc Binary files /dev/null and b/frontend/src/assets/sounds/achievement_unlock.wav differ diff --git a/frontend/src/assets/sounds/click.mp3 b/frontend/src/assets/sounds/click.mp3 new file mode 100644 index 0000000..5422c07 Binary files /dev/null and b/frontend/src/assets/sounds/click.mp3 differ diff --git a/frontend/src/assets/sounds/click2.mp3 b/frontend/src/assets/sounds/click2.mp3 new file mode 100644 index 0000000..95cda93 Binary files /dev/null and b/frontend/src/assets/sounds/click2.mp3 differ diff --git a/frontend/src/assets/sounds/click3.mp3 b/frontend/src/assets/sounds/click3.mp3 new file mode 100644 index 0000000..995fd86 Binary files /dev/null and b/frontend/src/assets/sounds/click3.mp3 differ diff --git a/frontend/src/assets/sounds/enemy_levendor_die.wav b/frontend/src/assets/sounds/enemy_levendor_die.wav new file mode 100644 index 0000000..f5d157a Binary files /dev/null and b/frontend/src/assets/sounds/enemy_levendor_die.wav differ diff --git a/frontend/src/assets/sounds/enemy_orc_die.wav b/frontend/src/assets/sounds/enemy_orc_die.wav new file mode 100644 index 0000000..9607f9e Binary files /dev/null and b/frontend/src/assets/sounds/enemy_orc_die.wav differ diff --git a/frontend/src/assets/sounds/enemy_scorpio_die.wav b/frontend/src/assets/sounds/enemy_scorpio_die.wav new file mode 100644 index 0000000..c8c4793 Binary files /dev/null and b/frontend/src/assets/sounds/enemy_scorpio_die.wav differ diff --git a/frontend/src/assets/sounds/enemy_wizard_die.wav b/frontend/src/assets/sounds/enemy_wizard_die.wav new file mode 100644 index 0000000..97ebe49 Binary files /dev/null and b/frontend/src/assets/sounds/enemy_wizard_die.wav differ diff --git a/frontend/src/assets/sounds/levelCompleted.wav b/frontend/src/assets/sounds/levelCompleted.wav new file mode 100644 index 0000000..1af34b1 Binary files /dev/null and b/frontend/src/assets/sounds/levelCompleted.wav differ diff --git a/frontend/src/assets/sounds/levelFailed.wav b/frontend/src/assets/sounds/levelFailed.wav new file mode 100644 index 0000000..97cd50e Binary files /dev/null and b/frontend/src/assets/sounds/levelFailed.wav differ diff --git a/frontend/src/assets/sounds/loseLife.wav b/frontend/src/assets/sounds/loseLife.wav new file mode 100644 index 0000000..5c65182 Binary files /dev/null and b/frontend/src/assets/sounds/loseLife.wav differ diff --git a/frontend/src/assets/sounds/modal_close.wav b/frontend/src/assets/sounds/modal_close.wav new file mode 100644 index 0000000..65e0daa Binary files /dev/null and b/frontend/src/assets/sounds/modal_close.wav differ diff --git a/frontend/src/assets/sounds/themes/Level_Under_Attack.mp3 b/frontend/src/assets/sounds/themes/Level_Under_Attack.mp3 new file mode 100644 index 0000000..5bc80eb Binary files /dev/null and b/frontend/src/assets/sounds/themes/Level_Under_Attack.mp3 differ diff --git a/frontend/src/assets/sounds/themes/Main_Theme.mp3 b/frontend/src/assets/sounds/themes/Main_Theme.mp3 new file mode 100644 index 0000000..ddee66e Binary files /dev/null and b/frontend/src/assets/sounds/themes/Main_Theme.mp3 differ diff --git a/frontend/src/assets/sounds/themes/credits.mp3 b/frontend/src/assets/sounds/themes/credits.mp3 new file mode 100644 index 0000000..d0f8f86 Binary files /dev/null and b/frontend/src/assets/sounds/themes/credits.mp3 differ diff --git a/frontend/src/assets/sounds/themes/level_1.mp3 b/frontend/src/assets/sounds/themes/level_1.mp3 new file mode 100644 index 0000000..12edaa3 Binary files /dev/null and b/frontend/src/assets/sounds/themes/level_1.mp3 differ diff --git a/frontend/src/assets/sounds/themes/level_2.mp3 b/frontend/src/assets/sounds/themes/level_2.mp3 new file mode 100644 index 0000000..a76e2ea Binary files /dev/null and b/frontend/src/assets/sounds/themes/level_2.mp3 differ diff --git a/frontend/src/assets/sounds/themes/level_3.mp3 b/frontend/src/assets/sounds/themes/level_3.mp3 new file mode 100644 index 0000000..b1b5b8f Binary files /dev/null and b/frontend/src/assets/sounds/themes/level_3.mp3 differ diff --git a/frontend/src/assets/sounds/themes/level_3_attack.mp3 b/frontend/src/assets/sounds/themes/level_3_attack.mp3 new file mode 100644 index 0000000..ff30331 Binary files /dev/null and b/frontend/src/assets/sounds/themes/level_3_attack.mp3 differ diff --git a/frontend/src/assets/sounds/themes/liinirea.mp3 b/frontend/src/assets/sounds/themes/liinirea.mp3 new file mode 100644 index 0000000..b30ceb6 Binary files /dev/null and b/frontend/src/assets/sounds/themes/liinirea.mp3 differ diff --git a/frontend/src/assets/sounds/tower_arrow_attack.wav b/frontend/src/assets/sounds/tower_arrow_attack.wav new file mode 100644 index 0000000..1579f7b Binary files /dev/null and b/frontend/src/assets/sounds/tower_arrow_attack.wav differ diff --git a/frontend/src/assets/sounds/tower_bomb_attack.wav b/frontend/src/assets/sounds/tower_bomb_attack.wav new file mode 100644 index 0000000..afe8f87 Binary files /dev/null and b/frontend/src/assets/sounds/tower_bomb_attack.wav differ diff --git a/frontend/src/assets/sounds/tower_building.wav b/frontend/src/assets/sounds/tower_building.wav new file mode 100644 index 0000000..ed2ce67 Binary files /dev/null and b/frontend/src/assets/sounds/tower_building.wav differ diff --git a/frontend/src/assets/sounds/tower_pick_place.mp3 b/frontend/src/assets/sounds/tower_pick_place.mp3 new file mode 100644 index 0000000..f983d12 Binary files /dev/null and b/frontend/src/assets/sounds/tower_pick_place.mp3 differ diff --git a/frontend/src/assets/sounds/tower_pick_place_2.mp3 b/frontend/src/assets/sounds/tower_pick_place_2.mp3 new file mode 100644 index 0000000..1c57be7 Binary files /dev/null and b/frontend/src/assets/sounds/tower_pick_place_2.mp3 differ diff --git a/frontend/src/assets/sounds/tower_sell.wav b/frontend/src/assets/sounds/tower_sell.wav new file mode 100644 index 0000000..8a1621d Binary files /dev/null and b/frontend/src/assets/sounds/tower_sell.wav differ diff --git a/frontend/src/assets/sounds/tower_wizard_attack.wav b/frontend/src/assets/sounds/tower_wizard_attack.wav new file mode 100644 index 0000000..e7ad6eb Binary files /dev/null and b/frontend/src/assets/sounds/tower_wizard_attack.wav differ diff --git a/frontend/src/assets/sounds/waveIncoming.wav b/frontend/src/assets/sounds/waveIncoming.wav new file mode 100644 index 0000000..2c7d07a Binary files /dev/null and b/frontend/src/assets/sounds/waveIncoming.wav differ diff --git a/frontend/src/assets/sounds/winStars.wav b/frontend/src/assets/sounds/winStars.wav new file mode 100644 index 0000000..9c9de18 Binary files /dev/null and b/frontend/src/assets/sounds/winStars.wav differ diff --git a/frontend/src/assets/sounds/wrong.mp3 b/frontend/src/assets/sounds/wrong.mp3 new file mode 100644 index 0000000..7612a57 Binary files /dev/null and b/frontend/src/assets/sounds/wrong.mp3 differ diff --git a/frontend/src/assets/sprites/creepy_die.png b/frontend/src/assets/sprites/creepy_die.png new file mode 100644 index 0000000..a47de64 Binary files /dev/null and b/frontend/src/assets/sprites/creepy_die.png differ diff --git a/frontend/src/assets/sprites/creepy_hurt.png b/frontend/src/assets/sprites/creepy_hurt.png new file mode 100644 index 0000000..78e38d8 Binary files /dev/null and b/frontend/src/assets/sprites/creepy_hurt.png differ diff --git a/frontend/src/assets/sprites/creepy_walk.png b/frontend/src/assets/sprites/creepy_walk.png new file mode 100644 index 0000000..d60a175 Binary files /dev/null and b/frontend/src/assets/sprites/creepy_walk.png differ diff --git a/frontend/src/assets/sprites/levendor_die.png b/frontend/src/assets/sprites/levendor_die.png new file mode 100644 index 0000000..7996cb2 Binary files /dev/null and b/frontend/src/assets/sprites/levendor_die.png differ diff --git a/frontend/src/assets/sprites/levendor_hurt.png b/frontend/src/assets/sprites/levendor_hurt.png new file mode 100644 index 0000000..56667d5 Binary files /dev/null and b/frontend/src/assets/sprites/levendor_hurt.png differ diff --git a/frontend/src/assets/sprites/levendor_walk.png b/frontend/src/assets/sprites/levendor_walk.png new file mode 100644 index 0000000..d1ee46f Binary files /dev/null and b/frontend/src/assets/sprites/levendor_walk.png differ diff --git a/frontend/src/assets/sprites/little-orc_die.png b/frontend/src/assets/sprites/little-orc_die.png new file mode 100644 index 0000000..9b2c5dc Binary files /dev/null and b/frontend/src/assets/sprites/little-orc_die.png differ diff --git a/frontend/src/assets/sprites/little-orc_hurt.png b/frontend/src/assets/sprites/little-orc_hurt.png new file mode 100644 index 0000000..c310d83 Binary files /dev/null and b/frontend/src/assets/sprites/little-orc_hurt.png differ diff --git a/frontend/src/assets/sprites/little-orc_walk.png b/frontend/src/assets/sprites/little-orc_walk.png new file mode 100644 index 0000000..95dd12f Binary files /dev/null and b/frontend/src/assets/sprites/little-orc_walk.png differ diff --git a/frontend/src/assets/sprites/mummy37x45.png b/frontend/src/assets/sprites/mummy37x45.png new file mode 100644 index 0000000..1be0749 Binary files /dev/null and b/frontend/src/assets/sprites/mummy37x45.png differ diff --git a/frontend/src/assets/sprites/scorpio_die.png b/frontend/src/assets/sprites/scorpio_die.png new file mode 100644 index 0000000..8e4820c Binary files /dev/null and b/frontend/src/assets/sprites/scorpio_die.png differ diff --git a/frontend/src/assets/sprites/scorpio_hurt.png b/frontend/src/assets/sprites/scorpio_hurt.png new file mode 100644 index 0000000..f26604d Binary files /dev/null and b/frontend/src/assets/sprites/scorpio_hurt.png differ diff --git a/frontend/src/assets/sprites/scorpio_walk.png b/frontend/src/assets/sprites/scorpio_walk.png new file mode 100644 index 0000000..bf9a0c3 Binary files /dev/null and b/frontend/src/assets/sprites/scorpio_walk.png differ diff --git a/frontend/src/assets/sprites/wizard-black_die.png b/frontend/src/assets/sprites/wizard-black_die.png new file mode 100644 index 0000000..1865ee2 Binary files /dev/null and b/frontend/src/assets/sprites/wizard-black_die.png differ diff --git a/frontend/src/assets/sprites/wizard-black_hurt.png b/frontend/src/assets/sprites/wizard-black_hurt.png new file mode 100644 index 0000000..4a7551e Binary files /dev/null and b/frontend/src/assets/sprites/wizard-black_hurt.png differ diff --git a/frontend/src/assets/sprites/wizard-black_walk.png b/frontend/src/assets/sprites/wizard-black_walk.png new file mode 100644 index 0000000..cca9965 Binary files /dev/null and b/frontend/src/assets/sprites/wizard-black_walk.png differ diff --git a/frontend/src/assets/towers/1.png b/frontend/src/assets/towers/1.png new file mode 100644 index 0000000..fdd151f Binary files /dev/null and b/frontend/src/assets/towers/1.png differ diff --git a/frontend/src/assets/towers/2 mirror.png b/frontend/src/assets/towers/2 mirror.png new file mode 100644 index 0000000..8f009b6 Binary files /dev/null and b/frontend/src/assets/towers/2 mirror.png differ diff --git a/frontend/src/assets/towers/2.png b/frontend/src/assets/towers/2.png new file mode 100644 index 0000000..c7fb81a Binary files /dev/null and b/frontend/src/assets/towers/2.png differ diff --git a/frontend/src/assets/towers/3.png b/frontend/src/assets/towers/3.png new file mode 100644 index 0000000..18b048e Binary files /dev/null and b/frontend/src/assets/towers/3.png differ diff --git a/frontend/src/assets/towers/4 mirror.png b/frontend/src/assets/towers/4 mirror.png new file mode 100644 index 0000000..d86eaad Binary files /dev/null and b/frontend/src/assets/towers/4 mirror.png differ diff --git a/frontend/src/assets/towers/4.png b/frontend/src/assets/towers/4.png new file mode 100644 index 0000000..d2824cb Binary files /dev/null and b/frontend/src/assets/towers/4.png differ diff --git a/frontend/src/assets/towers/5 mirror.png b/frontend/src/assets/towers/5 mirror.png new file mode 100644 index 0000000..84fdd09 Binary files /dev/null and b/frontend/src/assets/towers/5 mirror.png differ diff --git a/frontend/src/assets/towers/5.png b/frontend/src/assets/towers/5.png new file mode 100644 index 0000000..160d719 Binary files /dev/null and b/frontend/src/assets/towers/5.png differ diff --git a/frontend/src/assets/towers/6 mirror.png b/frontend/src/assets/towers/6 mirror.png new file mode 100644 index 0000000..80f7c08 Binary files /dev/null and b/frontend/src/assets/towers/6 mirror.png differ diff --git a/frontend/src/assets/towers/6.png b/frontend/src/assets/towers/6.png new file mode 100644 index 0000000..968caf1 Binary files /dev/null and b/frontend/src/assets/towers/6.png differ diff --git a/frontend/src/assets/towers/Archer.png b/frontend/src/assets/towers/Archer.png new file mode 100644 index 0000000..dcac694 Binary files /dev/null and b/frontend/src/assets/towers/Archer.png differ diff --git a/frontend/src/assets/towers/Artillery.png b/frontend/src/assets/towers/Artillery.png new file mode 100644 index 0000000..cf62ca8 Binary files /dev/null and b/frontend/src/assets/towers/Artillery.png differ diff --git a/frontend/src/assets/towers/Mage.png b/frontend/src/assets/towers/Mage.png new file mode 100644 index 0000000..22ce30a Binary files /dev/null and b/frontend/src/assets/towers/Mage.png differ diff --git a/frontend/src/assets/towers/arrow.png b/frontend/src/assets/towers/arrow.png new file mode 100644 index 0000000..a253804 Binary files /dev/null and b/frontend/src/assets/towers/arrow.png differ diff --git a/frontend/src/assets/towers/bomb.png b/frontend/src/assets/towers/bomb.png new file mode 100644 index 0000000..f275a1d Binary files /dev/null and b/frontend/src/assets/towers/bomb.png differ diff --git a/frontend/src/assets/towers/circle.png b/frontend/src/assets/towers/circle.png new file mode 100644 index 0000000..c8d27be Binary files /dev/null and b/frontend/src/assets/towers/circle.png differ diff --git a/frontend/src/assets/towers/circle_2d.png b/frontend/src/assets/towers/circle_2d.png new file mode 100644 index 0000000..ef4a482 Binary files /dev/null and b/frontend/src/assets/towers/circle_2d.png differ diff --git a/frontend/src/assets/towers/close_button_tower.png b/frontend/src/assets/towers/close_button_tower.png new file mode 100644 index 0000000..4f9fca0 Binary files /dev/null and b/frontend/src/assets/towers/close_button_tower.png differ diff --git a/frontend/src/assets/towers/magic.png b/frontend/src/assets/towers/magic.png new file mode 100644 index 0000000..1fab8ae Binary files /dev/null and b/frontend/src/assets/towers/magic.png differ diff --git a/frontend/src/assets/towers/missile-arrow.png b/frontend/src/assets/towers/missile-arrow.png new file mode 100644 index 0000000..927b620 Binary files /dev/null and b/frontend/src/assets/towers/missile-arrow.png differ diff --git a/frontend/src/assets/towers/missile-bomb.png b/frontend/src/assets/towers/missile-bomb.png new file mode 100644 index 0000000..6c05e2b Binary files /dev/null and b/frontend/src/assets/towers/missile-bomb.png differ diff --git a/frontend/src/assets/towers/missile-magic.png b/frontend/src/assets/towers/missile-magic.png new file mode 100644 index 0000000..3ca47ee Binary files /dev/null and b/frontend/src/assets/towers/missile-magic.png differ diff --git a/frontend/src/assets/towers/sale.png b/frontend/src/assets/towers/sale.png new file mode 100644 index 0000000..3693f10 Binary files /dev/null and b/frontend/src/assets/towers/sale.png differ diff --git a/frontend/src/assets/towers/tower.png b/frontend/src/assets/towers/tower.png new file mode 100644 index 0000000..342615c Binary files /dev/null and b/frontend/src/assets/towers/tower.png differ diff --git a/frontend/src/favicon.png b/frontend/src/favicon.png new file mode 100644 index 0000000..70efd02 Binary files /dev/null and b/frontend/src/favicon.png differ diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..12f75c7 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,13 @@ + + + + + + Kingdom Rush + + + + +
.
+ + \ No newline at end of file diff --git a/frontend/src/scripts/App.ts b/frontend/src/scripts/App.ts new file mode 100644 index 0000000..34e29cb --- /dev/null +++ b/frontend/src/scripts/App.ts @@ -0,0 +1,33 @@ +import '../styles/style.scss'; +import Phaser from 'phaser'; +import runAuth from './auth/run.auth'; +import config from './components/Game'; +import createBgGame from './auth/utils/create.bg'; +import { getPlayerStatsFromServer, PlayerStatsManager } from './components/stats/PlayerStats'; +import { KEY_ID } from './constants/constants'; + +export async function startApp() { + createBgGame(); + const playerStatsManager = new PlayerStatsManager(); + try { + const userId = localStorage.getItem(KEY_ID); + const data = await getPlayerStatsFromServer(userId); + playerStatsManager.prepopulateLocalStorage(data['data']); + } catch { + console.log('Something gone wrong with getting stats from backend'); + } + + if(!startApp.game) { + startApp.game = new Phaser.Game(config) + } else { + (document.querySelector('body') as HTMLBodyElement).style.height = '100%'; + (document.querySelector('canvas') as HTMLElement).style.display = ''; + startApp.game.loop.wake() + startApp.game.scene.wake('LevelsScene') + } +} +startApp.game = null; +window.addEventListener('load', () => { + runAuth(startApp); + document.querySelector('.fontPreload')?.remove() +}); diff --git a/frontend/src/scripts/LevelSettings.ts b/frontend/src/scripts/LevelSettings.ts new file mode 100644 index 0000000..1083571 --- /dev/null +++ b/frontend/src/scripts/LevelSettings.ts @@ -0,0 +1,20 @@ +import { levelsConfig } from './constants/constants'; + +export default class LevelSettings { + level: number; + + // мб переделать на enum ? + gameDifficulty: number; + + config: object; + + constructor(level: number, gameDifficulty: number) { + this.level = level; + this.gameDifficulty = gameDifficulty; + this.config = this.produceConfig(); + } + + produceConfig() { + return levelsConfig[`level_${this.level}`]; + } +} diff --git a/frontend/src/scripts/achievements/create.achievements.ts b/frontend/src/scripts/achievements/create.achievements.ts new file mode 100644 index 0000000..7c28ca0 --- /dev/null +++ b/frontend/src/scripts/achievements/create.achievements.ts @@ -0,0 +1,98 @@ +import createElement from '../auth/utils/createElement'; +import { whileLoad, whileRaise } from '../auth/utils/wait.while.loading'; +import popapProfileCreate from './create.popap.profile'; +import popapRatingCreate from './create.popap.rating'; +import langConfig from '../layouts/langConfig'; + +const SERVER = 'https://rs-clone.herokuapp.com'; + +async function getCurrentPlayerStats({ id, token }) { + const response = await fetch(`${SERVER}/users/${id}/stats`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + return response.json(); +} + +async function getPlayers({ token }) { + const response = await fetch(`${SERVER}/users`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + return response.json(); +} + +function popapSelectCreate({ stats, players, id }) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const statisticsText = langConfig[`${lang}`].statistics; + const profileText = langConfig[`${lang}`].profile; + const overallRatingText = langConfig[`${lang}`].overallRating; + + const popup = createElement('div', { + classList: ['popup-achievements-wrapper'], + innerHTML: ` + + `, + onclick: ({ target }) => { + if (target.classList.contains('popup-achievements-wrapper')) { + whileRaise(popup); + } + if (target.classList.contains('close-achievements-popup')) { + whileRaise(popup); + } + }, + }); + + const profile = createElement('div', { + classList: ['achievements-content-profile-button'], + textContent: profileText, + onclick: () => { + popup.remove(); + const [ userStat ] = stats.data.filter(({ userId }) => userId === id ); + popapProfileCreate(userStat); + }, + }); + + const rating = createElement('div', { + classList: ['achievements-content-rating-button'], + textContent: overallRatingText, + onclick: () => { + popup.remove(); + popapRatingCreate(stats.data, players.data); + }, + }); + + whileLoad(popup, '../assets/auth/achievement_board.png'); + + document.querySelector('.popup-achievements-content')?.append(profile, rating); +} + +function achievementsCreate({ id, token }) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const achievementsText = langConfig[`${lang}`].achievements; + + const achievementsIcon = createElement('div', { + classList: ['achievements-icon'], + textContent: `${achievementsText}`, + onclick: async () => { + const stats = await getCurrentPlayerStats({ id, token }); + const players = await getPlayers({ token }); + popapSelectCreate({ stats, players, id }); + }, + }); + + + const main = document.querySelector('main'); + main?.append(achievementsIcon); +} + +export default achievementsCreate; diff --git a/frontend/src/scripts/achievements/create.popap.profile.all.ts b/frontend/src/scripts/achievements/create.popap.profile.all.ts new file mode 100644 index 0000000..0503bfa --- /dev/null +++ b/frontend/src/scripts/achievements/create.popap.profile.all.ts @@ -0,0 +1,72 @@ +import createElement from '../auth/utils/createElement'; +import { whileLoad, whileRaise } from '../auth/utils/wait.while.loading'; +import langConfig from '../layouts/langConfig'; + +function popapProfileAllCreate(arrayStats) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const achievementsText = langConfig[`${lang}`].achievements; + const { achievs } = langConfig[`${lang}`]; + + const popup = createElement('div', { + classList: ['popup-profile-all-wrapper'], + innerHTML: ` + + `, + onclick: ({ target }) => { + if (target.classList.contains('popup-profile-all-wrapper')) { + whileRaise(popup); + } + if (target.classList.contains('close-profile-all-popup')) { + whileRaise(popup); + } + }, + }); + + whileLoad(popup, '../assets/interface/modal-bg.png'); +} + +export default popapProfileAllCreate; diff --git a/frontend/src/scripts/achievements/create.popap.profile.ts b/frontend/src/scripts/achievements/create.popap.profile.ts new file mode 100644 index 0000000..0e15db1 --- /dev/null +++ b/frontend/src/scripts/achievements/create.popap.profile.ts @@ -0,0 +1,131 @@ +import createElement from '../auth/utils/createElement'; +import { whileLoad, whileRaise } from '../auth/utils/wait.while.loading'; +import popapProfileAllCreate from './create.popap.profile.all'; +import langConfig from '../layouts/langConfig'; + +function popapProfileCreate(stats) { + const arrayStats = Object.entries(stats.achievements); + const allStats = Object.values(stats.achievements); + const gotStats = allStats.filter((property) => property); + const percent = ((allStats.length - gotStats.length) / allStats.length) * 100; + const achievement: Array = []; + + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const achievementsText = langConfig[`${lang}`].achievements; + const youGotText = langConfig[`${lang}`].youGot; + const achievementsOutOfText = langConfig[`${lang}`].achievementsOutOf; + const allText = langConfig[`${lang}`].all; + const { achievs } = langConfig[`${lang}`]; + let isAdd: boolean = false; + + const popup = createElement('div', { + classList: ['popup-profile-wrapper'], + innerHTML: ` + + `, + onclick: ({ target }) => { + if (target.classList.contains('popup-profile-wrapper')) { + whileRaise(popup); + } + if (target.classList.contains('close-profile-popup')) { + whileRaise(popup); + } + if (target.classList.contains('icon-achievements')) { + const [, need] = target.classList; + const iconsInfo = document.querySelectorAll('.wrapper-icon-achievements-info'); + + iconsInfo.forEach((el) => { + const needInfo = el.children[0].classList[1]; + if (need === needInfo) { + el.classList.remove('hide'); + el.classList.add('flex-for-achevements'); + } else { + el.classList.add('hide'); + el.classList.remove('flex-for-achevements'); + } + }); + } + if (target.classList.contains('all-achievements-button')) { + popup.remove(); + popapProfileAllCreate(arrayStats); + } + }, + }); + + whileLoad(popup, '../assets/modal-bg/start-modal-bg.png'); +} + +export default popapProfileCreate; diff --git a/frontend/src/scripts/achievements/create.popap.rating.ts b/frontend/src/scripts/achievements/create.popap.rating.ts new file mode 100644 index 0000000..9d8bda3 --- /dev/null +++ b/frontend/src/scripts/achievements/create.popap.rating.ts @@ -0,0 +1,139 @@ +import createElement from '../auth/utils/createElement'; +import { whileLoad, whileRaise } from '../auth/utils/wait.while.loading'; +import langConfig from '../layouts/langConfig'; + +function popapRatingCreate(stats, players) { + const forSort = stats.map((node) => { + const { gameProgress, achievements } = node; + + const allStatsAchievements = Object.values(achievements || {}); + const gotStatsAchievements = allStatsAchievements.filter((property) => property); + + const reducer = (acc, val) => acc + val; + const progress = Object.values(gameProgress || {}).reduce(reducer, 0); + + return { ...node, gameProgressSort: progress, achievementsSort: gotStatsAchievements.length }; + }); + + forSort.sort((a, b) => a.gameProgressSort - b.gameProgressSort).reverse(); + forSort.sort((a, b) => a.achievementsSort - b.achievementsSort).reverse(); + + players.forEach((player) => { + const isFind = forSort.find((node) => node.login === player.login); + if (!isFind) forSort.push(player); + }); + + const readyStats = forSort.map((node) => { + const { login, gameProgressSort, achievementsSort } = node; + const resultProgress = gameProgressSort === undefined ? '0' : gameProgressSort; + const resultAchievements = achievementsSort === undefined ? '0' : achievementsSort; + + return ` +
+
+ ${login} +
+
+ ${resultProgress}/9 +
+
+ ${resultAchievements}/8 +
+
+ ` + }); + + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const overallRatingText = langConfig[`${lang}`].overallRating; + const playerNameText = langConfig[`${lang}`].playerName; + const gameProgressText = langConfig[`${lang}`].gameProgress; + const achievementsText = langConfig[`${lang}`].achievements; + + const popup = createElement('div', { + classList: ['popup-rating-wrapper'], + innerHTML: ` + + `, + onclick: ({ target }) => { + if (target.classList.contains('popup-rating-wrapper')) { + whileRaise(popup); + } + if (target.classList.contains('close-rating-popup')) { + whileRaise(popup); + } + + const wrapper = document.querySelector('.wrapper-data-table-rating'); + const players = Array.from(document.querySelectorAll('.data-rating-player')); + const [name, progress, achievements] = Array.from(document.querySelectorAll('input[type="checkbox"]')); + + if (target.classList.contains('rating-property-name')) { + const sortHandler = (a, b) => a.getAttribute('data-name').localeCompare(b.getAttribute('data-name')); + + if (name.checked) { + players.sort(sortHandler); + } else { + players.sort(sortHandler).reverse(); + } + players.forEach((el) => { + wrapper?.append(el); + }); + } + + if (target.classList.contains('rating-property-progress')) { + const sortHandler = (a, b) => a.getAttribute('data-progress') - b.getAttribute('data-progress'); + + if (progress.checked) { + players.sort(sortHandler).reverse(); + } else { + players.sort(sortHandler); + } + players.forEach((el) => { + wrapper?.append(el); + }); + } + + if (target.classList.contains('rating-property-achievements')) { + const sortHandler = (a, b) => a.getAttribute('data-achievements') - b.getAttribute('data-achievements'); + + if (achievements.checked) { + players.sort(sortHandler).reverse(); + } else { + players.sort(sortHandler); + } + players.forEach((el) => { + wrapper?.append(el); + }); + } + }, + }); + + whileLoad(popup, '../assets/modal-bg/start-modal-bg.png'); +} + +export default popapRatingCreate; diff --git a/frontend/src/scripts/achievements/scss/_popap.achievements.profile.all.scss b/frontend/src/scripts/achievements/scss/_popap.achievements.profile.all.scss new file mode 100644 index 0000000..1b1ea59 --- /dev/null +++ b/frontend/src/scripts/achievements/scss/_popap.achievements.profile.all.scss @@ -0,0 +1,107 @@ +.popup-profile-all-wrapper { + position: fixed; + z-index: 15; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(41, 41, 41, 0.6); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.popup-profile-all-content { + background-image: url('../assets/interface/modal-bg.png'); + width: 630px; + height: 423px; + background-repeat: no-repeat; + background-size: cover; + border-radius: 5px; + cursor: auto; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 112px 30px 50px 30px; + transition: all .5s ease-out; + font-family: "Dimbo"; + color: #FFC107; + font-size: x-large; + letter-spacing: 0.8px; + bottom: 100%; +} + +.close-profile-all-popup { + background-image: url('../assets/interface/button_close.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} + +.title-profile-all { + color: rgb(230, 225, 82); + font-family: "Dimbo"; + letter-spacing: 1px; + margin: 0 20px; + background-image: url('../assets/auth/title.png'); + background-size: contain; + display: block; + width: 320px; + height: 131px; + text-align: center; + line-height: 97px; + font-size: xxx-large; + position: absolute; + top: -20px; +} + +.progress-profile-achievements-all { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 2.5px 2.5px; + padding: 16px; + + & .completeWin { background-image: url('../assets/achievements/complete_win.png'); } + & .firstBlood { background-image: url('../assets/achievements/first_blood.png'); } + & .greatDefender { background-image: url('../assets/achievements/great_defender.png'); } + & .ironDefender { background-image: url('../assets/achievements/iron_defender.png'); } + & .killer { background-image: url('../assets/achievements/killer.png'); } + & .seller { background-image: url('../assets/achievements/seller.png'); } + & .builder { background-image: url('../assets/achievements/builder.png'); } + & .firstAsterisk { background-image: url('../assets/achievements/first_asterisk.png'); } + + & .wrapper-icon-achievements-info { + display: flex; + align-items: center; + background-color: #a47248; + + & .icon-achievements-info { + width: 60px; + height: 60px; + padding: 3px; + box-sizing: border-box; + background-size: contain; + } + + & .icon-achievements-info-descriptions { + padding: 0 15px; + } + } +} + +.show-top { + bottom: 0; +} diff --git a/frontend/src/scripts/achievements/scss/_popap.achievements.profile.scss b/frontend/src/scripts/achievements/scss/_popap.achievements.profile.scss new file mode 100644 index 0000000..f1a274d --- /dev/null +++ b/frontend/src/scripts/achievements/scss/_popap.achievements.profile.scss @@ -0,0 +1,191 @@ +.popup-profile-wrapper { + position: fixed; + z-index: 15; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(41, 41, 41, 0.6); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.popup-profile-content { + background-image: url('../assets/modal-bg/start-modal-bg.png'); + width: 600px; + height: 423px; + background-repeat: no-repeat; + background-size: cover; + border-radius: 5px; + cursor: auto; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 112px 30px 20px 30px; + transition: all .5s ease-out; + font-family: "Dimbo"; + color: #e6e152; + font-size: x-large; + letter-spacing: 0.8px; + bottom: 100%; +} + +.close-profile-popup { + background-image: url('../assets/interface/button_close.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} + +.title-profile { + color: rgb(230, 225, 82); + font-family: "Dimbo"; + letter-spacing: 1px; + margin: 0 20px; + background-image: url('../assets/auth/title.png'); + background-size: contain; + display: block; + width: 320px; + height: 132px; + text-align: center; + line-height: 97px; + font-size: xxx-large; + position: absolute; + top: -20px; +} + +.progress-profile-achievements { + display: flex; + width: 100%; + justify-content: space-between; + + & .star-progress-profile { + background-image: url('../assets/achievements/achievements_icon_3.png'); + width: 90px; + height: 90px; + margin-top: -15px; + background-size: cover; + align-self: center; + } + + & .info-progress-profile { + & .progress-profile-bar { + background-image: url('../assets/auth/progress-bar.png'); + width: 425px; + height: 36px; + background-size: contain; + padding: 7px; + border-radius: 18px; + overflow: hidden; + + & .progress-profile-line { + fill: #FFC107; + transition: fill .3s ease; + border-radius: 15px; + + text { + fill: #352824; + } + } + } + } +} + +.icons-profile-achievements { + background-image: url('../assets/auth/UI_board_Small_parchment.png'); + width: 540px; + height: 204px; + background-size: contain; + padding: 35px 30px; + display: flex; + flex-direction: column; + + & .completeWin { background-image: url('../assets/achievements/complete_win.png'); } + & .firstBlood { background-image: url('../assets/achievements/first_blood.png'); } + & .greatDefender { background-image: url('../assets/achievements/great_defender.png'); } + & .ironDefender { background-image: url('../assets/achievements/iron_defender.png'); } + & .killer { background-image: url('../assets/achievements/killer.png'); } + & .seller { background-image: url('../assets/achievements/seller.png'); } + & .builder { background-image: url('../assets/achievements/builder.png'); } + & .firstAsterisk { background-image: url('../assets/achievements/first_asterisk.png'); } + + & .icons-profile { + display: flex; + flex-wrap: wrap; + height: 60px; + + & .icon-achievements { + width: 60px; + height: 60px; + padding: 3px; + box-sizing: border-box; + background-size: contain; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } + } + } + + & .icon-profile-info { + background-image: url('../assets/auth/field.png'); + width: 100%; + height: 65px; + background-size: contain; + margin-top: 7px; + padding: 2px; + + & .wrapper-icon-achievements-info { + position: absolute; + align-items: center; + + & .icon-achievements-info { + width: 60px; + height: 60px; + padding: 3px; + box-sizing: border-box; + background-size: contain; + } + + & .icon-achievements-info-descriptions { + padding: 0 15px; + } + } + } +} + +.all-achievements-button { + background-image: url('../assets/auth/btn.png'); + width: 140px; + height: 70px; + background-size: cover; + position: absolute; + bottom: -36px; + right: 36px; + text-align: center; + line-height: 70px; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} + +.show-top { + bottom: 0; +} diff --git a/frontend/src/scripts/achievements/scss/_popap.achievements.rating.scss b/frontend/src/scripts/achievements/scss/_popap.achievements.rating.scss new file mode 100644 index 0000000..8989133 --- /dev/null +++ b/frontend/src/scripts/achievements/scss/_popap.achievements.rating.scss @@ -0,0 +1,116 @@ +.popup-rating-wrapper { + position: fixed; + z-index: 15; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(41, 41, 41, 0.6); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.popup-rating-content { + background-image: url('../assets/modal-bg/start-modal-bg.png'); + width: 794px; + height: 560px; + background-repeat: no-repeat; + background-size: cover; + border-radius: 5px; + cursor: auto; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding: 112px 30px 50px 30px; + font-family: "Dimbo"; + color: #FFC107; + font-size: x-large; + letter-spacing: 0.8px; + transition: all .5s ease-out; + bottom: 100%; +} + +.close-rating-popup { + background-image: url('../assets/interface/button_close.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} + +.title-rating { + color: rgb(230, 225, 82); + font-family: "Dimbo"; + letter-spacing: 1px; + margin: 0 20px; + background-image: url('../assets/auth/title.png'); + background-size: contain; + width: 224px; + height: 93px; + text-align: center; + line-height: 59px; + font-size: xx-large; + position: absolute; + top: -20px; +} + +.wrapper-table-rating { + width: 100%; + position: absolute; + top: 70px; + padding: 0 50px; + + & .title-rating-property { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + justify-items: right; + + & input { + display: none; + } + + & > * { + cursor: pointer; + + &:hover { + transform: scale(1.1); + } + } + } +} + +.wrapper-data-table-rating { + width: 100%; + height: 100%; + padding: 0 20px; + overflow-y: scroll; + + & .data-rating-player { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + justify-items: right; + } + + &::-webkit-scrollbar { + width: 5px; + } + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 1px rgb(212, 232, 37); + } + &::-webkit-scrollbar-thumb { + background: goldenrod; + border-radius: 10px; + } +} diff --git a/frontend/src/scripts/achievements/scss/_popap.achievements.scss b/frontend/src/scripts/achievements/scss/_popap.achievements.scss new file mode 100644 index 0000000..b3e82a8 --- /dev/null +++ b/frontend/src/scripts/achievements/scss/_popap.achievements.scss @@ -0,0 +1,87 @@ +.popup-achievements-wrapper { + position: fixed; + z-index: 15; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(41, 41, 41, 0.6); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.popup-achievements-content { + background-image: url('../assets/auth/achievement_board.png'); + width: 557px; + height: 250px; + background-repeat: no-repeat; + background-size: cover; + border-radius: 5px; + display: flex; + cursor: auto; + position: relative; + display: flex; + justify-content: center; + align-items: center; + bottom: 100%; + transition: all .5s ease-out; +} + +.title-rating { + color: rgb(230, 225, 82); + font-family: "Dimbo"; + letter-spacing: 1px; + margin: 0 20px; + background-image: url('../assets/auth/title.png'); + background-size: contain; + width: 207px; + height: 85px; + text-align: center; + line-height: 59px; + font-size: xx-large; + position: absolute; + top: -20px; +} + +.close-achievements-popup { + background-image: url('../assets/interface/button_close.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} + +.achievements-content-profile-button, +.achievements-content-rating-button { + background-image: url('../assets/auth/btn.png'); + width: 175px; + height: 93px; + background-size: cover; + margin: 0 40px; + font-family: "Dimbo"; + color: #e6e152; + font-size: x-large; + letter-spacing: 0.8px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + &:hover { + transform: scale(1.1); + } +} + +.show-top { + bottom: 0; +} diff --git a/frontend/src/scripts/achievements/scss/achievements.scss b/frontend/src/scripts/achievements/scss/achievements.scss new file mode 100644 index 0000000..f0bd351 --- /dev/null +++ b/frontend/src/scripts/achievements/scss/achievements.scss @@ -0,0 +1,28 @@ +@import './popap.achievements'; +@import './popap.achievements.rating'; +@import './popap.achievements.profile'; +@import './popap.achievements.profile.all'; + +.achievements-icon { + background-image: url('../assets/achievements/achievements_icon.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 126px; + height: 126px; + position: fixed; + margin: 10px; + bottom: 50px; + right: 0; + color: #e6e152; + font-family: "Dimbo"; + text-align: center; + font-size: x-large; + line-height: 189px; + z-index: 2; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } +} diff --git a/frontend/src/scripts/achievements/utils/backend.ts b/frontend/src/scripts/achievements/utils/backend.ts new file mode 100644 index 0000000..ccc5162 --- /dev/null +++ b/frontend/src/scripts/achievements/utils/backend.ts @@ -0,0 +1,31 @@ +import { KEY_ID, KEY_TOKEN, LOCAL_STORAGE_KEY } from '../../constants/constants'; +import { getCurrentPlayerStats, setCurrentPlayerStats } from '../../backend'; + +async function sendDataToBackend() { + const id = localStorage.getItem(KEY_ID); + const token = localStorage.getItem(KEY_TOKEN); + const currentStorage = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || ""); + const { data } = await getCurrentPlayerStats({ id, token }); + const { userId, login } = data; + + const objectWillSend = { + id, + token, + body: { + ...data, + userId, + login, + builtTowers: currentStorage.builtTowers, + soldTowers: currentStorage.soldTowers, + killedEnemies: currentStorage.killedEnemies, + achievements: currentStorage.achievements, + gameProgress: currentStorage.gameProgress, + ironModeProgress: currentStorage.ironModeProgress, + }, + }; + + const update = await setCurrentPlayerStats(objectWillSend); + return update; +} + +export default sendDataToBackend; diff --git a/frontend/src/scripts/attendance/backend/getAttendance.ts b/frontend/src/scripts/attendance/backend/getAttendance.ts new file mode 100644 index 0000000..59d9d88 --- /dev/null +++ b/frontend/src/scripts/attendance/backend/getAttendance.ts @@ -0,0 +1,8 @@ +async function getAttendance() { + const url = 'https://rs-clone.herokuapp.com/'; + + const res = await fetch(`${url}chart`); + return res.json(); +} + +export default getAttendance; diff --git a/frontend/src/scripts/attendance/backend/handleAttendent.ts b/frontend/src/scripts/attendance/backend/handleAttendent.ts new file mode 100644 index 0000000..6400766 --- /dev/null +++ b/frontend/src/scripts/attendance/backend/handleAttendent.ts @@ -0,0 +1,41 @@ +const url = 'https://rs-clone.herokuapp.com/'; +const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + +const fullDate = new Date(); +const date = fullDate.getDate(); +const month = fullDate.getMonth(); +const year = fullDate.getFullYear(); + +const stringId = `${addZero(date)}${addZero(month)}${year}`; +const stringDate = `${date} ${MONTHS[month]} ${year}`; + +function addZero(n) { + return (parseInt(n, 10) < 10 ? '0' : '') + n; +} + +export default async function handlerAttendance() { + const response = await fetch(`${url}chart/${stringId}`); + const { data, ok } = await response.json(); + + if (!ok) { + const response = await fetch(`${url}chart`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: stringId, attendance: 1, date: stringDate }), + }); + return response.json(); + } else { + const response = await fetch(`${url}chart/${stringId}`, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ...data, attendance: data.attendance + 1 }), + }); + return response.json(); + } +} diff --git a/frontend/src/scripts/attendance/create.attendance.ts b/frontend/src/scripts/attendance/create.attendance.ts new file mode 100644 index 0000000..05f2245 --- /dev/null +++ b/frontend/src/scripts/attendance/create.attendance.ts @@ -0,0 +1,54 @@ +import createElement from "../auth/utils/createElement"; +import { whileLoad, whileRaise } from "../auth/utils/wait.while.loading"; +import langConfig from '../layouts/langConfig'; + +function createPopupAttendance(arr) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const attendanceOverYearText = langConfig[`${lang}`].attendanceOverYear; + + const maxAttendance = Math.max(...arr.map((el) => el.attendance)); + const popup = createElement('div', { + classList: ['popup-attendance-wrapper'], + innerHTML: ` + + `, + onclick: ({ target }) => { + if (target.classList.contains('popup-attendance-wrapper')) { + whileRaise(popup); + } + if (target.classList.contains('close-popup')) { + whileRaise(popup); + } + }, + }); + + whileLoad(popup, '../assets/interface/modal-bg.png'); + const content = document.querySelector('.day-attendance-content'); + if (content) content.scrollTop = 999; +} + +export default createPopupAttendance; diff --git a/frontend/src/scripts/attendance/scss/_graph.scss b/frontend/src/scripts/attendance/scss/_graph.scss new file mode 100644 index 0000000..db32e31 --- /dev/null +++ b/frontend/src/scripts/attendance/scss/_graph.scss @@ -0,0 +1,64 @@ +.title-attandence-popap { + font-size: xx-large; + color: #352824; + margin-bottom: 30px; +} + +.day-attendance-content { + display: flex; + flex-direction: column; + align-items: center; + height: 200px; + padding-right: 10px; + overflow-y: scroll; + + &::-webkit-scrollbar { + width: 5px; + } + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 1px rgb(212, 232, 37); + } + &::-webkit-scrollbar-thumb { + background: goldenrod; + border-radius: 10px; + } +} + +svg { + overflow: visible; + height: 20px; +} + +.day-attendance-info { + display: flex; + justify-content: space-between; + align-items: center; + width: 500px; +} + +.bar { + fill: #a27448; + height: 20px; + transition: fill .3s ease; + cursor: pointer; + + text { + fill: #352824; + } +} + +.chart:hover, +.chart:focus { + .bar { + fill: #aaa; + } +} + +.bar:hover, +.bar:focus { + fill: #352824 !important; + + text { + fill: whitesmoke; + } +} diff --git a/frontend/src/scripts/attendance/scss/_popap.scss b/frontend/src/scripts/attendance/scss/_popap.scss new file mode 100644 index 0000000..7711432 --- /dev/null +++ b/frontend/src/scripts/attendance/scss/_popap.scss @@ -0,0 +1,47 @@ +.popup-attendance-wrapper { + position: fixed; + z-index: 15; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(41, 41, 41, 0.6); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.popup-attendance-content { + background-image: url('../assets/interface/modal-bg.png'); + width: 658px; + height: 441px; + background-repeat: no-repeat; + background-size: cover; + border-radius: 5px; + display: flex; + flex-direction: column; + cursor: auto; + position: relative; + justify-content: center; + align-items: center; + transition: all .5s ease-out; + bottom: 100%; +} + +.close-popup { + background-image: url('../assets/interface/button_close.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; +} + +.show-top { + bottom: 0; +} diff --git a/frontend/src/scripts/attendance/scss/create.attendance.scss b/frontend/src/scripts/attendance/scss/create.attendance.scss new file mode 100644 index 0000000..1348991 --- /dev/null +++ b/frontend/src/scripts/attendance/scss/create.attendance.scss @@ -0,0 +1,2 @@ +@import './popap.scss'; +@import './graph.scss'; \ No newline at end of file diff --git a/frontend/src/scripts/auth/run.auth.ts b/frontend/src/scripts/auth/run.auth.ts new file mode 100644 index 0000000..b29bfbe --- /dev/null +++ b/frontend/src/scripts/auth/run.auth.ts @@ -0,0 +1,35 @@ +import createSignPage from './utils/create.sign'; +import createStartPage from './utils/create.start'; +import handleAttendent from '../attendance/backend/handleAttendent'; + +import { KEY_TOKEN, KEY_ID } from '../constants/constants'; + +const url = 'https://rs-clone.herokuapp.com/'; + +function runAuth(fn) { + const token = localStorage.getItem(KEY_TOKEN); + const id = localStorage.getItem(KEY_ID); + + handleAttendent(); + + if (id) { + fetch(`${url}users/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }).then(({ ok }) => { + if (ok) { + createStartPage({ id, token }); + document.querySelector('.logo-start-button')?.addEventListener('click', fn); + } else { + createSignPage(); + } + }); + } else { + createSignPage(); + } +} + +export default runAuth; diff --git a/frontend/src/scripts/auth/scss/_lang-switcher.scss b/frontend/src/scripts/auth/scss/_lang-switcher.scss new file mode 100644 index 0000000..ea9f7c9 --- /dev/null +++ b/frontend/src/scripts/auth/scss/_lang-switcher.scss @@ -0,0 +1,60 @@ +.lang { + &-switcher { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + } + + &-dropdown { + display: none; + &:hover { + display: block; + } + } + + &-item { + display: flex; + justify-content: space-around; + height: 50px; + width: 120px; + padding: 5px; + background: #34495eb8; + color: #ffffff; + align-items: center; + cursor: pointer; + } + + &-selecting { + &:first-child { + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + &:last-child { + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + } + &:hover { + background: #22313f; + display: flex; + } + } + + &-flag { + height: 100%; + } + + &-text { + font-family: 'Dimbo'; + margin: 0; + } + + &-current { + display: flex; + border-radius: 10px; + margin-bottom: 1px; + &:hover ~ .lang-dropdown { + display: block; + } + } +} diff --git a/frontend/src/scripts/auth/scss/_loader.scss b/frontend/src/scripts/auth/scss/_loader.scss new file mode 100644 index 0000000..5dc25c0 --- /dev/null +++ b/frontend/src/scripts/auth/scss/_loader.scss @@ -0,0 +1,18 @@ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loader { + border: 16px solid #f3f3f3; + border-top: 16px solid #FFC107; + border-radius: 50%; + width: 120px; + height: 120px; + animation: spin 2s linear infinite; + align-self: center; + margin: auto; + position: absolute; + left: calc(50% - 60px); + z-index: 1000; +} diff --git a/frontend/src/scripts/auth/scss/auth.scss b/frontend/src/scripts/auth/scss/auth.scss new file mode 100644 index 0000000..084df29 --- /dev/null +++ b/frontend/src/scripts/auth/scss/auth.scss @@ -0,0 +1,413 @@ +@import '../../attendance/scss/create.attendance.scss'; +@import './loader.scss'; + + +* { + box-sizing: border-box; + margin: 0; +} + +body { + font-family: "Dimbo"; + overflow-y: hidden; +} + +.blur-bg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: -1; + + height: 100%; + width: 100%; + + background-image: url("../assets/auth/kingdom-rush.png"); + background-size: cover; + background-repeat: repeat; + background-position: center; + + filter: blur(15px); + -webkit-filter: blur(15px); + opacity: 0.8; +} + +.sign-kingdom-rush, +.start-page { + display: flex; + position: relative; + justify-content: center; + align-items: center; + + height: 100vh; + width: 100%; + + margin: auto; + + background-image: url("../assets/auth/kingdom-rush.png"); + background-size: cover; + background-repeat: no-repeat; + background-position: top center; +} + +.logo-start-button, +.logo-credits-button { + background-image: url("../assets/auth/btn.png"); + background-size: cover; + margin: auto; + position: relative; + font-family: "Dimbo"; + letter-spacing: 1px; + color: #fbd738; + text-align: center; + + cursor: pointer !important; + &:hover { + transform: scale(1.1); + } +} + +.logo-start-button { + width: 230px; + height: 122px; + font-size: xxx-large; + line-height: 122px; + margin-bottom: 10px; +} + +.logo-credits-button { + width: 230px; + height: 122px; + font-size: xx-large; + line-height: 122px; + margin-bottom: 10px; +} + +.logout-game { + background-image: url("../assets/interface/button_close.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 70px; + height: 70px; + position: fixed; + z-index: 2; + margin: 10px; + + z-index: 2; + cursor: pointer; + &:hover { + transform: scale(1.1); + } +} + +.attendance-per-year-game { + background-image: url("../assets/interface/button_menu.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 70px; + height: 70px; + position: fixed; + margin: 10px; + bottom: 50px; + z-index: 2; + cursor: pointer; + &:hover { + transform: scale(1.1); + } +} + +.wrapper-logo-start-page { + width: 408px; + height: 500px; +} + +.logo-start-page { + background-image: url("../assets/auth/logo.png"); + width: 408px; + height: 292px; + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} + +.sign-window { + width: 620px; + height: 438px; + background-size: cover; + background-image: url("../assets/modal-bg/start-modal-bg.png"); + padding: 0 60px; + & .sign-info { + height: 100px; + display: flex; + align-items: center; + position: relative; + + & .response-info { + font-family: "Dimbo"; + color: #e6e152; + font-size: x-large; + letter-spacing: 0.8px; + position: absolute; + bottom: -10px; + margin: 0 20px; + } + } +} + +.sign-info-in, +.sign-info-up { + color: rgb(230, 225, 82); + cursor: pointer; + font-family: "Dimbo"; + letter-spacing: 1px; + background-image: url("../assets/auth/title.png"); + background-size: contain; + display: block; + width: 226px; + height: 94px; + text-align: center; + line-height: 74px; + font-size: x-large; + position: absolute; + top: -9px; + + &:hover { + transform: scale(1.2); + } +} + +.sign-info-in { + left: 0; +} + +.sign-info-up { + right: 0; +} + +.sign-form { + font-family: "Dimbo"; + letter-spacing: 0.8px; + font-size: xx-large; + display: flex; + flex-direction: column; + position: relative; + + & .wrapper-username, + .wrapper-password { + display: flex; + flex-direction: column; + & input { + font-family: "Dimbo"; + letter-spacing: 0.8px; + font-size: xx-large; + color: rgb(230, 225, 82); + height: 67px; + border: none; + background-image: url("../assets/auth/field.png"); + padding: 0 0 0 20px; + background-size: cover; + background-color: #6e3c24; + margin: 10px 0; + } + + & input::placeholder { + font-family: "Dimbo"; + letter-spacing: 0.8px; + font-size: xx-large; + color: rgb(230, 225, 82); + } + } + + & .keep-me { + font-size: x-large; + + & input { + position: absolute; + display: none; + &:checked + label { + color: #ffc107; + } + &:checked + label > .checkbox-image { + background-image: url("../assets/sign/onNoText.png"); + } + &:checked + label > .checkbox-text { + transform: scale(1.1); + } + } + + & label { + color: rgb(230, 225, 82); + display: inline-flex; + margin: 10px 0; + cursor: pointer; + + & .checkbox-image { + width: 84px; + height: 40px; + background-image: url("../assets/sign/offNoText.png"); + background-size: cover; + margin-right: 20px; + } + } + } + + & input.sign-in-submit { + height: 150px; + width: 278px; + border: none; + background: url("../assets/auth/btn.png"); + background-size: cover; + cursor: pointer; + font-family: "Dimbo"; + letter-spacing: 0.8px; + font-size: xx-large; + color: #ffc107; + align-self: center; + position: relative; + bottom: -22px; + + &:hover { + transform: scale(1.1); + } + } +} + +.active-sign-info { + cursor: default; + color: #ffc107; + transform: scale(1.2); + + &:hover { + transform: scale(1.2); + } +} + +main { + height: calc(100vh - 50px); +} + +footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; + + height: 50px; + padding: 5px 15px; + + background: url('../assets/overlay_60.png'), url('../assets/auth/kingdom-rush.png'); + background-repeat: no-repeat; + background-position: bottom; + background-size: cover; + + font-size: 16px; + color: #fbd738; + position: relative; + z-index: 3; + cursor: default; + transition: all 250ms linear; + + .the-rolling-scopes { + display: flex; + flex-direction: row; + align-items: center; + + margin: auto auto auto 0; + + .rss { + width: 100px; + height: 40px; + margin: 0 5px; + + background: url('../assets/icons/rs_school_js.svg'); + background-size: contain; + background-repeat: no-repeat; + + opacity: 0.8; + &:hover { + border-bottom: 2px solid transparent; + opacity: 1; + } + } + } + + .wrapper-team-people { + display: flex; + flex-direction: row; + justify-content: space-between; + + margin: auto; + + .team-people { + padding: 0 5px; + + a { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + & a>* { + display: inline-block; + } + + .avatar { + width: 40px; + height: 40px; + margin: 0 5px; + + background-size: cover; + background-repeat: no-repeat; + border-radius: 50%; + + &.Iogsotot { + background-image: url("https://avatars.githubusercontent.com/u/50149163?s=400&u=9a763fefd25839d62bec939ad7302d8a29948cb4&v=4"); + } + &.DenisAfa { + background-image: url("https://avatars.githubusercontent.com/u/64201928?s=400&u=b33f2c5f182a021649e8ac548259a05dc3ed792e&v=4"); + } + &.Abdulloh76 { + background-image: url("https://avatars.githubusercontent.com/u/59783836?s=400&v=4"); + } + &.mrINEX { + background-image: url("https://avatars.githubusercontent.com/u/35580404?s=400&u=742a3a17f24933ff191b620cf949e6f1b872e20a&v=4"); + } + } + } + } + + .year-create { + margin: auto 0 auto auto; + font-size: 24px; + } + + a { + display: block; + + color: #fbd738; + border-bottom: 2px solid transparent; + text-decoration: none; + + opacity: 0.8; + cursor: pointer; + transition: all 250ms linear; + + &:hover { + opacity: 1; + color: #fbad38; + border-bottom: 2px solid #fbad38; + transition: all 250ms linear; + } + } +} + +.hide { + display: none; +} + +.flex-for-achevements { + display: flex; +} diff --git a/frontend/src/scripts/auth/utils/LangSwitcher.ts b/frontend/src/scripts/auth/utils/LangSwitcher.ts new file mode 100644 index 0000000..0d85bb1 --- /dev/null +++ b/frontend/src/scripts/auth/utils/LangSwitcher.ts @@ -0,0 +1,83 @@ +import createElement from './createElement'; +import switchStartPageLang from './switch.start-page.lang'; + +export interface LangConfig { + lang: string; + text: string; +} + +export default class LangSwitcher { + configs: LangConfig[] + switcherContainer: HTMLElement + langCurrent: HTMLElement + dropDown: HTMLElement + + constructor(configs: LangConfig[]) { + this.configs = configs; + this.switcherContainer = createElement('div', { classList: ['lang-switcher'] }); + this.langCurrent = createElement('div', { classList: ['lang-current', 'lang-item'] }); + this.dropDown = createElement('div', { classList: ['lang-dropdown'] }); + } + + init() { + if (localStorage.getItem('lang')) { + window['lang'] = localStorage.getItem('lang'); + } else { + window['lang'] = 'en'; + localStorage.setItem('lang', window['lang']); + } + + const currentLang = window['lang']; + + this.langCurrent.setAttribute('data-lang', currentLang); + + for (let i = 0; i < this.configs.length; i++) { + if (this.configs[i].lang === currentLang) { + this.langCurrent.innerHTML = this.langItemInner(this.configs[i]); + } else { + const lang = createElement('div', { classList: ['lang-selecting', 'lang-item'], 'data-lang': this.configs[i].lang }); + lang.innerHTML = this.langItemInner(this.configs[i]); + this.dropDown.append(lang); + } + } + + this.switcherContainer.append(this.langCurrent); + this.switcherContainer.append(this.dropDown); + + this.dropDown.addEventListener('click', this.clickHandler); + + return this.switcherContainer; + } + + langItemInner(conf: LangConfig) { + return ` + +

${conf.text}

+ `; + } + + clickHandler = (e) => { + let selectingLang = e.target.closest('.lang-selecting'); + + selectingLang.classList.remove('lang-selecting'); + this.langCurrent.classList.remove('lang-current'); + + selectingLang.remove(); + this.langCurrent.remove(); + + const tmp = this.langCurrent; + this.langCurrent = selectingLang; + selectingLang = tmp; + + selectingLang.classList.add('lang-selecting'); + this.langCurrent.classList.add('lang-current'); + + window['lang'] = this.langCurrent.dataset.lang; + localStorage.setItem('lang', window['lang']); + + switchStartPageLang(window['lang']); + + this.switcherContainer.prepend(this.langCurrent); + this.dropDown.append(selectingLang); + } +} diff --git a/frontend/src/scripts/auth/utils/create.bg.ts b/frontend/src/scripts/auth/utils/create.bg.ts new file mode 100644 index 0000000..d7ba584 --- /dev/null +++ b/frontend/src/scripts/auth/utils/create.bg.ts @@ -0,0 +1,24 @@ +import createElement from './createElement'; + +function createBgGame() { + document.querySelector('main')?.remove(); + document.querySelector('footer')?.remove(); + + if (document.querySelector('.blur-bg')) { return; } + + const blurBg = createElement( + 'div', + { + classList: ['blur-bg'], + }, + { + height: `${window.innerHeight}`, + width: `${window.innerWidth}`, + }, + ); + + // document.body.textContent = ''; + document.body.append(blurBg); +} + +export default createBgGame; diff --git a/frontend/src/scripts/auth/utils/create.sign.ts b/frontend/src/scripts/auth/utils/create.sign.ts new file mode 100644 index 0000000..eb3d6bb --- /dev/null +++ b/frontend/src/scripts/auth/utils/create.sign.ts @@ -0,0 +1,111 @@ +import createElement from './createElement'; +import { signUp, signIn } from '../../backend'; +import langConfig from '../../layouts/langConfig'; + +function createSignPage() { + // pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W|_])[a-zA-Z0-9_\W]{8,}$" + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const signInText = langConfig[`${lang}`].signIn.toUpperCase(); + const signUpText = langConfig[`${lang}`].signUp.toUpperCase(); + const nameText = langConfig[`${lang}`].name; + const passwordText = langConfig[`${lang}`].password; + const upperNameText = nameText[0].toUpperCase() + nameText.slice(1); + const upperPasswordText = passwordText[0].toUpperCase() + passwordText.slice(1); + const keepMeText = langConfig[`${lang}`].keepMe; + + const signPage = createElement( + 'div', + { + classList: ['sign-kingdom-rush'], + innerHTML: ` +
+ +
+ ${signInText} + ${signUpText} +
+ +
+
+ +
+
+ +
+
+ + +
+ +
+
+ `, + }, + { + height: `${window.innerHeight}`, + width: `${window.innerWidth}`, + } + ); + + const responseInfo = createElement('div', { + classList: ['response-info'], + }); + + const body = document.querySelector('body') as HTMLBodyElement; + body.textContent = ''; + body.append(signPage); + + const signInfoIn = document.querySelector('.sign-info-in') as HTMLElement; + const signInfoUp = document.querySelector('.sign-info-up') as HTMLElement; + + signInfoUp?.after(responseInfo); + + const signSubmit = document.querySelector('.sign-in-submit') as HTMLInputElement; + + function handler({ target }) { + signSubmit.value = target.textContent; + signInfoIn.classList.remove('active-sign-info'); + signInfoUp.classList.remove('active-sign-info'); + target.classList.add('active-sign-info'); + } + + signInfoIn.onclick = handler; + signInfoUp.onclick = handler; + + signSubmit.onclick = (e) => { + const { elements } = document.forms.namedItem('signForm') as HTMLFormElement; + const username = (elements.namedItem('username') as HTMLInputElement).value; + const password = (elements.namedItem('password') as HTMLInputElement).value; + + const active = document.querySelector('.active-sign-info') as HTMLSpanElement; + const isIn = active.classList.contains('sign-info-in'); + const isUp = active.classList.contains('sign-info-up'); + + if (username && password) { + e.preventDefault(); + + isIn && signIn({ login: username, password }); + isUp && signUp({ login: username, password }); + } + }; +} + +export default createSignPage; diff --git a/frontend/src/scripts/auth/utils/create.start.ts b/frontend/src/scripts/auth/utils/create.start.ts new file mode 100644 index 0000000..899a2a7 --- /dev/null +++ b/frontend/src/scripts/auth/utils/create.start.ts @@ -0,0 +1,109 @@ +import createElement from './createElement'; +import getAttendance from '../../attendance/backend/getAttendance'; +import createCredits from '../../credits/create.credits'; +import { KEY_ID, KEY_TOKEN } from '../../constants/constants'; +import LangSwitcher, { LangConfig } from './LangSwitcher'; +import createPopupAttendance from '../../attendance/create.attendance'; +import achievementsCreate from '../../achievements/create.achievements'; +import langConfig from '../../layouts/langConfig'; + +const langConfigs: LangConfig[] = [ + { lang: 'en', text: 'English' }, + { lang: 'ru', text: 'Русский' }, + { lang: 'uz', text: 'O\'zbekcha' }, +] + +function createStartPage({ id, token }) { + const body = document.querySelector('body') as HTMLBodyElement; + // body.innerText = ''; + const main = createElement('main'); + + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const startText = langConfig[`${lang}`].start.toUpperCase(); + const creditsText = langConfig[`${lang}`].credits.toUpperCase(); + + const startPage = createElement( + 'div', + { + classList: ['start-page'], + innerHTML: ` +
+
+
${startText}
+
${creditsText}
+
+ `, + }, + { + height: `${window.innerHeight}`, + width: `${window.innerWidth}`, + } + ); + + const logout = createElement('div', { + classList: ['logout-game'], + onclick: () => { + localStorage.removeItem(KEY_TOKEN); + localStorage.removeItem(KEY_ID); + window.location.reload(); + }, + }); + + const attendance = createElement('div', { + classList: ['attendance-per-year-game'], + onclick: async () => { + const res = await getAttendance(); + const { data } = res; + createPopupAttendance(data); + }, + }); + + const footer = createElement('footer', { + classList: ['kingdom-rush-footer'], + innerHTML: ` +
+ +
+ + + +
2021
+ ` + }); + + main.append(new LangSwitcher(langConfigs).init(), logout, attendance, startPage); + + body.append(main, footer); + + const credits = document.querySelector('.logo-credits-button'); + credits?.addEventListener('click', createCredits); + + achievementsCreate({ id, token }); +} + +export default createStartPage; diff --git a/frontend/src/scripts/auth/utils/createElement.ts b/frontend/src/scripts/auth/utils/createElement.ts new file mode 100644 index 0000000..07426c9 --- /dev/null +++ b/frontend/src/scripts/auth/utils/createElement.ts @@ -0,0 +1,16 @@ +export default function createElement(type, attributes = {}, styles = {}) { + const element = document.createElement(type); + Object.keys(attributes).forEach((key) => { + if (key === 'classList') { + element.classList.add(...attributes[key]); + } else if (/data-/.test(key)) { + element.setAttribute(key, attributes[key]); + } else { + element[key] = attributes[key]; + } + }); + Object.keys(styles).forEach((key) => { + element.style[key] = styles[key]; + }); + return element; +} diff --git a/frontend/src/scripts/auth/utils/switch.start-page.lang.ts b/frontend/src/scripts/auth/utils/switch.start-page.lang.ts new file mode 100644 index 0000000..579734c --- /dev/null +++ b/frontend/src/scripts/auth/utils/switch.start-page.lang.ts @@ -0,0 +1,19 @@ +import langConfig from '../../layouts/langConfig'; + +function switchStartPageLang(lang) { + const startButton = document.querySelector('.logo-start-button'); + const creditsButton = document.querySelector('.logo-credits-button'); + const achievementsButton = document.querySelector('.achievements-icon'); + + const startText = langConfig[`${lang}`].start.toUpperCase(); + const creditsText = langConfig[`${lang}`].credits.toUpperCase(); + const achievementsText = langConfig[`${lang}`].achievements; + + if (startButton && creditsButton && achievementsButton) { + startButton['textContent'] = startText; + creditsButton['textContent'] = creditsText; + achievementsButton['textContent'] = achievementsText; + } +} + +export default switchStartPageLang; diff --git a/frontend/src/scripts/auth/utils/wait.while.loading.ts b/frontend/src/scripts/auth/utils/wait.while.loading.ts new file mode 100644 index 0000000..475e846 --- /dev/null +++ b/frontend/src/scripts/auth/utils/wait.while.loading.ts @@ -0,0 +1,25 @@ +import createElement from './createElement'; + +function whileLoad(element, imgUrl) { + const loader = createElement('div', { + classList: ['loader'], + }); + + element.append(loader); + document.querySelector('body')?.append(element); + + const preloaderImg = document.createElement('img'); + preloaderImg.src = imgUrl; + + preloaderImg.addEventListener('load', () => { + loader.remove(); + element.children[0].classList.add('show-top'); + }); +} + +function whileRaise(element) { + element.children[0].classList.remove('show-top'); + element.children[0].addEventListener("transitionend", () => element.remove(), false); +} + +export { whileLoad, whileRaise }; diff --git a/frontend/src/scripts/backend.ts b/frontend/src/scripts/backend.ts new file mode 100644 index 0000000..a68ef41 --- /dev/null +++ b/frontend/src/scripts/backend.ts @@ -0,0 +1,173 @@ +import { startApp } from './App'; +import createStartPage from './auth/utils/create.start'; +import { KEY_TOKEN, KEY_ID } from './constants/constants'; +import langConfig from './layouts/langConfig'; + +const SERVER = 'https://rs-clone.herokuapp.com'; + +async function signIn(user) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const hasSignInText = langConfig[`${lang}`].hasSignIn; + + const responseInfo = document.querySelector('.response-info') as HTMLElement; + + const url = `${SERVER}/login`; + const options = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(user), + }; + + try { + const { checked } = document.forms + .namedItem('signForm') + ?.elements.namedItem('scales') as HTMLInputElement; + + const form = document.querySelector('.sign-form') as HTMLFormElement; + + const response = await fetch(url, options); + + const { data, token, login, ok, id } = await response.json(); + + if (ok) { + responseInfo.innerHTML = `${login} ${hasSignInText}`; + + localStorage.setItem(KEY_ID, id); + localStorage.setItem(KEY_TOKEN, token); + + if (!checked) { + window.addEventListener('unload', function() { + localStorage.removeItem(KEY_ID); + localStorage.removeItem(KEY_TOKEN); + }); + } + + const isStats = await getCurrentPlayerStats({ id, token }); + + if (!isStats.ok) { + createStats({ id, token, login }); + } else { + setCurrentPlayerStats({ + id, + token, + body: { ...isStats.data, gameLogInCount: isStats.data.gameLogInCount + 1 }, + }); + } + + document.body.textContent = ''; + createStartPage({ id, token }); + document.querySelector('.logo-start-button')?.addEventListener('click', startApp); + } else { + responseInfo.textContent = data; + form.reset(); + } + } catch (err) { + responseInfo.textContent = err.name; + } +} + +async function getCurrentPlayerStats({ id, token }) { + try { + const response = await fetch(`${SERVER}/users/${id}/stats/current`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + return response.json(); + } catch(err) { + return { data: err, ok: false }; + } +} + +async function setCurrentPlayerStats({ id, token, body }) { + try { + const response = await fetch(`${SERVER}/users/${id}/stats/`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + return response.json(); + } catch(err) { + return { data: err, ok: false }; + } +} + +async function createStats({ id, token, login }) { + try { + const url = `${SERVER}/users/${id}/stats`; + const options = { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ userId: id, login }), + }; + + const response = await fetch(url, options); + return response.json(); + } catch(err) { + return { data: err, ok: false }; + } +} + +async function signUp(user) { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const hasSignUpText = langConfig[`${lang}`].hasSignUp; + const signInText = langConfig[`${lang}`].signIn.toUpperCase(); + const signUpText = langConfig[`${lang}`].signUp.toUpperCase(); + + const url = `${SERVER}/logup`; + const options = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(user), + }; + + const responseInfo = document.querySelector('.response-info') as HTMLElement; + + const request = new Request(url, options); + + try { + const signInfoIn = document.querySelector('.sign-info-in'); + const signInfoUp = document.querySelector('.sign-info-up'); + const signSubmit = document.querySelector('.sign-in-submit') as HTMLInputElement; + const form = document.querySelector('.sign-form') as HTMLFormElement; + + const response = await fetch(request); + const { data, ok } = await response.json(); + + if (ok) { + responseInfo.innerHTML = `${data.login} ${hasSignUpText}`; + form.reset(); + + setTimeout(() => { + signInfoIn?.classList.add('active-sign-info'); + signInfoUp?.classList.remove('active-sign-info'); + signSubmit.value = signInText; + }, 300); + } else { + responseInfo.textContent = data; + form.reset(); + } + } catch (err) { + responseInfo.textContent = err.name; + } +} + +export { signIn, signUp }; + +export { getCurrentPlayerStats, setCurrentPlayerStats }; diff --git a/frontend/src/scripts/components/Gate.ts b/frontend/src/scripts/components/Gate.ts new file mode 100644 index 0000000..a41f977 --- /dev/null +++ b/frontend/src/scripts/components/Gate.ts @@ -0,0 +1,7 @@ +export default class Gate extends Phaser.GameObjects.Sprite { + constructor(scene: Phaser.Scene, x: number, y: number, texture: string) { + super(scene, x, y, texture); + scene.add.existing(this); + this.setInteractive(); + } +} diff --git a/frontend/src/scripts/components/button/Button.ts b/frontend/src/scripts/components/button/Button.ts new file mode 100644 index 0000000..6ba2998 --- /dev/null +++ b/frontend/src/scripts/components/button/Button.ts @@ -0,0 +1,39 @@ +export default class Button extends Phaser.GameObjects.Container { + btnImage: Phaser.GameObjects.Image; + + constructor(scene: Phaser.Scene, x: number, y: number, btnTexture: string) { + super(scene, x, y); + scene.add.existing(this); + + this.btnImage = scene.add.image(0, 0, btnTexture).setScale(0.8); + + this.add(this.btnImage); + + this.setSize(this.btnImage.width, this.btnImage.height); + this.init(); + } + + init() { + this.setInteractive({ useHandCursor: true }) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, this.handleUp, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_OUT, this.handleOut, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_DOWN, this.handleDown, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_OVER, this.handleOver, this); + } + + handleOver() { + this.setScale(1.15).setAlpha(1); + } + + handleOut() { + this.setScale(1); + } + + handleDown() { + // this.handleOver() + } + + handleUp() { + this.handleOut(); + } +} diff --git a/frontend/src/scripts/components/button/CustomButton.ts b/frontend/src/scripts/components/button/CustomButton.ts new file mode 100644 index 0000000..8fd0847 --- /dev/null +++ b/frontend/src/scripts/components/button/CustomButton.ts @@ -0,0 +1,60 @@ +import Button from './Button'; + +export default class CustomButton extends Button { + btnDownImage: Phaser.GameObjects.Image; + + btnText: Phaser.GameObjects.Text; + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + text: string, + upTexture: string, + downTexture: string, + ) { + super(scene, x, y, upTexture); + scene.add.existing(this); + + this.btnDownImage = scene.add.image(0, 0, downTexture); + this.btnDownImage.setPosition(0, this.btnImage.height - this.btnDownImage.height); + const styles = { fontFamily: 'Dimbo', fontSize: '60px' }; + this.btnText = scene.add.text(0, 0, text, styles).setOrigin(0.5); + this.btnText.setShadow(3, 3, '#000000'); + this.btnText.setTint(0xfafafa, 0xfafafa, 0x8f8f8f, 0x8f8f8f); + this.btnDownImage.setVisible(false); + + this.add(this.btnDownImage); + this.add(this.btnText); + + this.init(); + } + + init() { + this.setInteractive({ useHandCursor: true }) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, this.handleUp, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_OUT, this.handleOut, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_DOWN, this.handleDown, this) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_OVER, this.handleOver, this); + } + + handleOver() { + this.btnText.setTint(0x8f8f8f, 0x8f8f8f, 0xfafafa, 0xfafafa); + } + + handleOut() { + this.btnDownImage.setVisible(false); + this.btnImage.setVisible(true); + this.btnText.setTint(0xfafafa, 0xfafafa, 0x8f8f8f, 0x8f8f8f); + } + + handleDown() { + this.handleOver(); + this.btnDownImage.setVisible(true); + this.btnImage.setVisible(false); + } + + handleUp() { + this.handleOut(); + } +} diff --git a/frontend/src/scripts/components/button/DiffButton.ts b/frontend/src/scripts/components/button/DiffButton.ts new file mode 100644 index 0000000..c497a9f --- /dev/null +++ b/frontend/src/scripts/components/button/DiffButton.ts @@ -0,0 +1,45 @@ +import CustomButton from './CustomButton'; +import langConfig from '../../layouts/langConfig'; +export default class DiffButton extends CustomButton { + easyBtn: Phaser.GameObjects.Image + + hardBtn: Phaser.GameObjects.Image + + diffBtns: string[][] + + constructor(scene: Phaser.Scene, x: number, y: number) { + const lang = window['lang']; + const normal = langConfig[`${lang}`].normal.toUpperCase(); + + super(scene, x, y, `${normal}`, 'normal-btn-bg', 'normal-btn-bg'); + + this.diffBtns = [ + ['easy', langConfig[`${lang}`].easy], + ['normal', langConfig[`${lang}`].normal], + ['hard', langConfig[`${lang}`].hard], + ]; + + this.setInteractive({ useHandCursor: true }) + .on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, this.handleClick, this); + + this.scene.input.keyboard.on('keydown-D', (event) => { + this.handleDown(); + this.handleClick(); + }); + this.scene.input.keyboard.on('keyup-D', (event) => { + this.handleUp(); + }); + } + + handleClick() { + const visibleBtnIndex = this.diffBtns.findIndex((el) => el[1].toUpperCase() === this.btnText.text); + const index = (visibleBtnIndex + 1) % this.diffBtns.length; + this.btnImage.setTexture(`${this.diffBtns[index][0]}-btn-bg`); + this.btnDownImage.setTexture(`${this.diffBtns[index][0]}-btn-bg`); + this.btnText.setText(`${this.diffBtns[index][1].toUpperCase()}`); + } + + getDifficulty() { + return this.diffBtns.findIndex((el) => el[1].toUpperCase() === this.btnText.text) + 1; + } +} diff --git a/frontend/src/scripts/components/button/LevelButton.ts b/frontend/src/scripts/components/button/LevelButton.ts new file mode 100644 index 0000000..d13d87b --- /dev/null +++ b/frontend/src/scripts/components/button/LevelButton.ts @@ -0,0 +1,75 @@ +import Button from './Button'; +import { levelsConfig } from '../../constants/constants'; +import StartScreenModal from '../modal/StartScreenModal'; + +export default class LevelButton extends Button { + level: number; + + constructor(scene: Phaser.Scene, x: number, y: number, btnTexture: string, level: number) { + super(scene, x, y, btnTexture); + this.level = level; + + this.btnImage.setScale(1.1); + this.scene.input.keyboard.on(`keydown-${numToStr(level)}`, (event) => { + this.handleDown(); + }); + } + + handleDown() { + const modal = new StartScreenModal( + this.scene, + levelsConfig[`level_${this.level}`].map.scaleCoordinateTowers.length, + this.level, + ); + modal.slideIn(); + + modal.startBtn.setInteractive().on('pointerup', () => { + // get difficulty + const diff = modal.difficultyBtn.getDifficulty(); + modal.slideOut(); + this.scene.cameras.main.fadeOut(500, 0, 0, 0); + this.scene.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.time.delayedCall(500, () => { + this.scene.scene.start('game-scene', { level: this.level, gameDifficulty: diff }); + }); + }); + }); + this.scene.input.keyboard.on('keydown-ENTER', (event) => { + // get difficulty + const diff = modal.difficultyBtn.getDifficulty(); + modal.slideOut(); + this.scene.cameras.main.fadeOut(500, 0, 0, 0); + this.scene.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.time.delayedCall(500, () => { + this.scene.scene.start('game-scene', { level: this.level, gameDifficulty: diff }); + }); + }); + }); + + modal.closeModalBtn.setInteractive().on('pointerup', () => { + modal.slideOut(); + }); + + this.scene.input.keyboard.on('keydown-ESC', (event) => { + modal.slideOut(); + }); + } + handleOut() { + this.setScale(1).setAlpha(0.8); + } +} + +function numToStr(number: number) { + switch (number) { + case 0: return 'ZERO'; + case 1: return 'ONE'; + case 2: return 'TWO'; + case 3: return 'THREE'; + case 4: return 'FOUR'; + case 5: return 'FIVE'; + case 6: return 'SIX'; + case 7: return 'SEVEN'; + case 8: return 'EIGHT'; + case 9: return 'NINE'; + } +} \ No newline at end of file diff --git a/frontend/src/scripts/components/button/WaveButton.ts b/frontend/src/scripts/components/button/WaveButton.ts new file mode 100644 index 0000000..71fdca6 --- /dev/null +++ b/frontend/src/scripts/components/button/WaveButton.ts @@ -0,0 +1,10 @@ +export default class WaveButton extends Phaser.GameObjects.PathFollower { + constructor(scene: Phaser.Scene, path: Phaser.Curves.Path, x: number, y: number, texture: string) { + super(scene, path, x, y, texture); + scene.add.existing(this); + this.setInteractive(); + this.setScale(0.75); + // this.x = x; + // this.y = y; + } +} diff --git a/frontend/src/scripts/components/events/achievements_popup.ts b/frontend/src/scripts/components/events/achievements_popup.ts new file mode 100644 index 0000000..d1e7a3e --- /dev/null +++ b/frontend/src/scripts/components/events/achievements_popup.ts @@ -0,0 +1,96 @@ +export default class Popup extends Phaser.GameObjects.Container { + achievementBg: Phaser.GameObjects.Sprite; + popupHeight: number; + + constructor(scene: Phaser.Scene, x: number, y: number, achievementTexture: string) { + super(scene, x, y); + scene.add.existing(this); + this.x = scene.cameras.main.centerX / 2; + this.popupHeight = 250; + this.y = this.popupHeight / 2; + this.achievementBg = scene.add.sprite(this.x, -this.popupHeight, achievementTexture); + this.setSize(this.achievementBg.width, this.achievementBg.height); + this.y = this.height / 2; + this.achievementBg.setY(-this.height) + this.add(this.achievementBg); + } + + init(type) { + let iconTexture = ''; + let text = ''; + const styles = { + fontFamily: 'Dimbo', + fontSize: '60px', + color: '#d2a252', + }; + switch (type) { + case 'builder': + text = 'Builder'; + iconTexture = 'icon-builder'; + break; + case 'completeWin': + text = 'complete win!'; + iconTexture = 'icon-complete_win'; + break; + case 'firstAsterisk': + text = 'First asterisk'; + iconTexture = 'icon-first_asterisk'; + break; + case 'firstBlood': + text = 'First blood'; + iconTexture = 'icon-first_blood'; + break; + case 'greatDefender': + text = 'Great defender!'; + iconTexture = 'icon-great_defender'; + break; + case 'killer': + text = 'Killer'; + iconTexture = 'icon-killer'; + break; + case 'ironDefender': + text = 'Iron defender'; + iconTexture = 'icon-iron_defender'; + break; + case 'seller': + text = 'Seller'; + iconTexture = 'icon-seller'; + break; + default: + text = 'Great defender'; + iconTexture = 'icon-first_asterisk'; + } + const icon = this.scene.add.sprite(this.x, -this.popupHeight, iconTexture).setOrigin(0.5, 0.8).setScale(0.7); + const title = this.scene.add.text(this.x, -this.popupHeight, text, styles).setOrigin(0.5, -0.4); + this.add([icon, title]); + } + + startAnimation() { + this.scene.sound.play('achievement-unlock'); + this.slideIn(); + + setTimeout(() => { + this.slideOut(); + }, 3000); + } + + slideIn() { + this.scene.tweens.add({ + targets: this, + y: { start: 0, to: this.popupHeight * 1.5 }, + ease: 'Cubic.Out', + repeat: 0, + duration: 800, + }); + } + + slideOut() { + this.scene.tweens.add({ + targets: this, + y: { start: this.popupHeight * 1.5, to: 0 }, + ease: 'Cubic.Out', + repeat: 0, + duration: 800, + }); + } +} \ No newline at end of file diff --git a/frontend/src/scripts/components/game.ts b/frontend/src/scripts/components/game.ts new file mode 100644 index 0000000..d82d280 --- /dev/null +++ b/frontend/src/scripts/components/game.ts @@ -0,0 +1,36 @@ +import 'phaser'; +import LevelsScene from './scenes/LevelsScene'; +import PreloadScene from './scenes/PreloadScene'; +import GameScene from './scenes/GameScene'; +import PauseScene from './scenes/PauseScene'; +import LoseScene from './scenes/LoseScene'; +import WinScene from './scenes/WinScene'; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.CANVAS, + backgroundColor: '#000000', + scale: { + parent: 'phaser-game', + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_HORIZONTALLY, + width: 2048, + height: 1210, + }, + scene: [ + PreloadScene, + LevelsScene, + PauseScene, + WinScene, + LoseScene, + GameScene, + ], + physics: { + default: 'arcade', + arcade: { + // debug: true, + gravity: { y: 0 }, + }, + }, +}; + +export default config; diff --git a/frontend/src/scripts/components/interface/AudioSlider.ts b/frontend/src/scripts/components/interface/AudioSlider.ts new file mode 100644 index 0000000..44d0a17 --- /dev/null +++ b/frontend/src/scripts/components/interface/AudioSlider.ts @@ -0,0 +1,174 @@ +import Button from '../button/Button'; +import GameScene from '../scenes/GameScene'; + +interface BarConfigs { + border: number; + containerCoordinates: number[]; + barCoordinates: number[]; + barSizes: number[]; + borderRadius: number; + maxValue: number; +} + +export default class AudioSlider extends Phaser.GameObjects.Container { + title: Phaser.GameObjects.Text; + + checkbox: Phaser.GameObjects.Image; + + increase: Button; + + decrease: Button; + + barContainer: Phaser.GameObjects.Image; + + progressBar: Phaser.GameObjects.Graphics; + + type: string; + + barConfigs: BarConfigs; + + constructor(scene: Phaser.Scene, x: number, y: number, titleTexture: string, type:string) { + super(scene, x, y); + + this.type = type; + const styles = { + fontFamily: 'Dimbo', + fontSize: '80px', + color: '#dbc899', + }; + this.title = scene.add.text(0, 0, titleTexture, styles); + this.add(this.title); + + this.initCheckbox(scene); + this.initProgressBar(scene); + + this.increase.setInteractive().on('pointerup', () => this.drawProgressBar(1)); + this.decrease.setInteractive().on('pointerup', () => this.drawProgressBar(-1)); + } + + initCheckbox(scene: Phaser.Scene) { + this.checkbox = scene.add.image(this.title.width * 1.2, this.title.height, 'on').setOrigin(0,1); + this.add(this.checkbox); + this.checkbox + .setInteractive({ useHandCursor: true }) + .on('pointerup', this.handleCheckboxClick, this); + } + + handleCheckboxClick() { + if (this.checkbox.texture.key === 'on') { + this.checkbox.setTexture('off'); + this.drawProgressBar(-10); + } else { + this.checkbox.setTexture('on'); + this.drawProgressBar(1); + } + } + + initProgressBar(scene: Phaser.Scene) { + this.decrease = new Button(scene, 0, this.title.height * 1.5, 'minus'); + this.decrease.setX(this.decrease.width / 2); + + this.barContainer = scene.add.image(0, this.title.height * 1.5, 'slider-bar-bg'); + + this.barContainer.setX(this.barContainer.width / 2 + this.decrease.x * 2); + + this.progressBar = scene.add.graphics(); + this.initBarConfigs(); + + const audios = (this.scene.scene.get('game-scene') as GameScene)[`${this.type}`]; + const audioKeys = Object.keys(audios); + const volume = audios[`${audioKeys[0]}`].volume; + for(let i = 0; i < audioKeys.length; i++) { + audios[`${audioKeys[i]}`].setVolume(volume); + } + this.drawProgressBar(volume * 10); + + this.increase = new Button( + scene, + this.barContainer.width + this.decrease.width * 1.5, + this.title.height * 1.5, + 'plus', + ); + + this.add(this.decrease); + this.add(this.increase); + this.add(this.barContainer); + this.add(this.progressBar); + } + + initBarConfigs() { + const border = 10; + const containerCoordinates = [ + this.barContainer.x - this.barContainer.width / 2 + border, + this.barContainer.y - this.barContainer.height / 2 + border, + ]; + const barCoordinates = [ + containerCoordinates[0], + containerCoordinates[1] + (this.barContainer.height - 2 * border) / 2, + ]; + const barSizes = [this.barContainer.width - 2 * border, this.barContainer.height - 2 * border]; + const borderRadius = this.barContainer.height / 4; + + this.barConfigs = { + border, + containerCoordinates, + barCoordinates, + barSizes, + borderRadius, + maxValue: barSizes[0], + }; + this.barConfigs.barSizes[0] = 0; + } + + drawProgressBar(change: number) { + if (this.barConfigs.barSizes[0] === 20 && change === -1) return; + else if (this.barConfigs.barSizes[0] === this.barConfigs.maxValue && change === 1) return; + + this.barConfigs.barSizes[0] += 50 * change; + + if (this.barConfigs.barSizes[0] < 20) { + this.barConfigs.barSizes[0] = 20; + } else if (this.barConfigs.barSizes[0] > this.barConfigs.maxValue) { + this.barConfigs.barSizes[0] = this.barConfigs.maxValue; + } + + if (this.barConfigs.barSizes[0] > 20) this.checkbox.setTexture('on'); + const audios = (this.scene.scene.get('game-scene') as GameScene)[`${this.type}`]; + const audioKeys = Object.keys(audios); + for (let i = 0; i < audioKeys.length; i++) { + audios[`${audioKeys[i]}`].setVolume(this.barConfigs.barSizes[0] / this.barConfigs.maxValue); + } + + this.progressBar.clear(); + // bar + let color = this.barConfigs.barSizes[0] > this.barConfigs.maxValue * 0.8 ? 0xe65540 : 0xf4d133; + this.progressBar.fillStyle(color, 1); + this.progressBar.fillRoundedRect( + this.barConfigs.containerCoordinates[0], + this.barConfigs.containerCoordinates[1], + this.barConfigs.barSizes[0], + this.barConfigs.barSizes[1], + this.barConfigs.borderRadius, + ); + // shadow + color = this.barConfigs.barSizes[0] > this.barConfigs.maxValue * 0.8 ? 0xc63f31 : 0xde9b26; + this.progressBar.fillStyle(color, 1); + this.progressBar.fillRoundedRect( + this.barConfigs.barCoordinates[0], + this.barConfigs.barCoordinates[1], + this.barConfigs.barSizes[0], + this.barConfigs.barSizes[1] / 2, + { + tl: 0, + tr: 0, + bl: this.barConfigs.borderRadius, + br: this.barConfigs.borderRadius, + }, + ); + + // yellow + // 0xf4d133 0xde9b26 + // red + // 0xe65540 0xc63f31 + } +} diff --git a/frontend/src/scripts/components/interface/GameObjStats.ts b/frontend/src/scripts/components/interface/GameObjStats.ts new file mode 100644 index 0000000..b1f1492 --- /dev/null +++ b/frontend/src/scripts/components/interface/GameObjStats.ts @@ -0,0 +1,225 @@ +import Tower from '../tower/Tower'; +import Unit from '../unit/Unit'; +import langConfig from '../../layouts/langConfig'; + +export default class GameObjStats extends Phaser.GameObjects.Container { + gameObject: Tower | Unit; + + objNameContainer: Phaser.GameObjects.Graphics; + + objInfoContainer: Phaser.GameObjects.Graphics; + + modalConfigs: any; + + objImg: Phaser.GameObjects.Image; + + nameText: Phaser.GameObjects.Text; + + infoText_1: Phaser.GameObjects.Text; + + infoText_2: Phaser.GameObjects.Text; + + infoText_3: Phaser.GameObjects.Text; + + infoImg_1: Phaser.GameObjects.Image; + + infoImg_2: Phaser.GameObjects.Image; + + infoImg_3: Phaser.GameObjects.Image; + + constructor(scene: Phaser.Scene) { + super(scene, scene.cameras.main.centerX, scene.cameras.main.centerY - 80); + scene.add.existing(this); + this.objNameContainer = scene.add.graphics(); + this.objInfoContainer = scene.add.graphics(); + + this.setSize(+scene.sys.game.config.width / 2, 60); + this.setPosition( + scene.cameras.main.centerX - this.width / 2, + +scene.sys.game.config.height - this.height * 0.7, + ); + + this.drawContainers(); + this.generate(); + this.depth = 1000; + } + + drawContainers() { + this.objNameContainer.setPosition(0, -this.height * 0.7); + const nameContainerWidth = this.width * 0.38; + const borderColor = 0x593517; + const bgColor = 0x42250f; + this.objNameContainer.fillStyle(bgColor); + this.objNameContainer.fillRoundedRect(0, 0, nameContainerWidth, this.height, 10); + this.objNameContainer.lineStyle(10, borderColor); + this.objNameContainer.strokeRoundedRect(0, 0, nameContainerWidth, this.height, 10); + this.objNameContainer.setAlpha(0.8); + + const circles = this.scene.add.graphics(); + circles.setPosition(0, -this.height * 0.7 + this.height * 0.5); + circles.fillStyle(0x000000); + circles.fillCircle(0, 0, this.height * 0.8); + circles.fillStyle(0xd2cdcc); + circles.fillCircle(0, 0, this.height * 0.73); + circles.fillStyle(0x000000); + circles.fillCircle(0, 0, this.height * 0.62); + circles.setAlpha(0.9); + + const infoContainerWidth = this.width * 0.58; + this.objInfoContainer.setPosition(this.width * 0.4, -this.height * 0.7); + + this.objInfoContainer.fillStyle(bgColor); + this.objInfoContainer.fillRoundedRect(0, 0, infoContainerWidth, this.height, 10); + this.objInfoContainer.lineStyle(10, borderColor); + this.objInfoContainer.strokeRoundedRect(0, 0, infoContainerWidth, this.height, 10); + this.objInfoContainer.setAlpha(0.8); + + this.modalConfigs = { + nameContainerWidth, + infoContainerWidth, + animStart: +this.scene.sys.game.config.height * 1.2, + animTo: +this.scene.sys.game.config.height - this.height * 0.7, + }; + + this.add(this.objNameContainer); + this.add(circles); + this.add(this.objInfoContainer); + } + + generate() { + const fontStyles = { + fontFamily: 'Dimbo', + fontSize: '40px', + align: 'center', + }; + + this.objImg = this.scene.add.image(0, 0, '').setOrigin(0.5); + this.nameText = this.scene.add.text(0, 0, '', fontStyles).setOrigin(0.44, 0.5); + + this.infoImg_1 = this.scene.add.image(0, 0, '').setOrigin(0.5, 0.5); + this.infoText_1 = this.scene.add.text(0, 0, '', fontStyles).setOrigin(0, 0.5); + + this.infoImg_2 = this.scene.add.image(0, 0, '').setOrigin(0, 0.5); + this.infoText_2 = this.scene.add.text(0, 0, '', fontStyles).setOrigin(0, 0.5); + + this.infoImg_3 = this.scene.add.image(0, 0, '').setOrigin(0, 0.5); + this.infoText_3 = this.scene.add.text(0, 0, '', fontStyles).setOrigin(0, 0.5); + + this.add(this.objImg); + this.add(this.nameText); + this.add(this.infoImg_1); + this.add(this.infoText_1); + this.add(this.infoImg_2); + this.add(this.infoText_2); + this.add(this.infoImg_3); + this.add(this.infoText_3); + + this.slideOut(); + } + + infoConfig(obj: Tower | Unit) { + const textConfig = langConfig[`${window['lang']}`]; + if (obj instanceof Unit) { + return { + avaTexture: obj.unitType, + name: textConfig.enemy[obj.unitType].toUpperCase(), + img1: 'heart-icon', + text1: `${obj.hp}/${obj.maxHp}`, + img2: 'shoes-icon', + text2: + obj.moveSpeed < 30000 + ? textConfig.fast + : obj.moveSpeed < 45000 + ? textConfig.medium + : textConfig.slow, + img3: 'coins-icon', + text3: `${obj.killReward}`, + }; + } else if (obj instanceof Tower) { + if (!obj.isTowerBuilt || !obj.type) return null; + obj.canSale(this.slideOut, this); + const missile = obj.getType() === 'archers' ? 'arrow' : obj.getType() === 'artillery' ? 'bomb' : 'magic'; + return { + avaTexture: `${missile}-icon`, + name: `${textConfig.tower[obj.getType()]}`, + img1: 'damage-icon', + text1: `${obj.damage}`, + img2: 'hour-glass-icon', + text2: + obj.timeForNextShot > 2400 + ? textConfig.slow + : obj.timeForNextShot > 1400 + ? textConfig.medium + : textConfig.fast, + img3: 'target-icon', + text3: + obj.attackArea < 350 + ? textConfig.small + : obj.attackArea < 400 + ? textConfig.medium + : textConfig.long, + }; + } + } + + updateStats(obj: Tower | Unit) { + const infoConfig = this.infoConfig(obj); + if (!infoConfig) return; + this.gameObject = obj; + this.slideIn(); + this.objImg.setTexture(infoConfig.avaTexture); + this.objImg.setPosition(0, -this.height * 0.7 + this.height * 0.5); + this.objImg.displayHeight = this.height * 1.3; + this.objImg.scaleX = this.objImg.scaleY; + + const textsY = -this.nameText.height / 4; // because they have identical styles + this.nameText.setText(infoConfig.name); + this.nameText.setPosition(this.modalConfigs.nameContainerWidth / 2, textsY); + + this.infoImg_1.setTexture(infoConfig.img1); + this.infoText_1.setText(infoConfig.text1); + this.infoImg_1.setPosition(this.objInfoContainer.x + this.infoImg_1.width / 2, textsY); + this.infoText_1.setPosition(this.infoImg_1.x + this.infoImg_1.width/2, textsY); + + this.infoImg_2.setTexture(infoConfig.img2); + this.infoText_2.setText(infoConfig.text2); + const info2_xStart = this.objInfoContainer.x + + this.modalConfigs.infoContainerWidth / 2 + - this.infoText_2.width/2; + this.infoImg_2.setPosition(info2_xStart - this.infoImg_2.width, textsY); + this.infoText_2.setPosition(info2_xStart, textsY); + + this.infoImg_3.setTexture(infoConfig.img3); + this.infoText_3.setText(infoConfig.text3); + const info3_xStart = this.objInfoContainer.x + this.modalConfigs.infoContainerWidth - this.infoText_3.width-10; + this.infoImg_3.setPosition(info3_xStart - this.infoImg_3.width, textsY); + this.infoText_3.setPosition(info3_xStart, textsY); + } + + slideIn() { + this.scene.tweens.add({ + targets: this, + y: { start: this.modalConfigs.animStart, to: this.modalConfigs.animTo }, + ease: 'Cubic.Out', + repeat: 0, + duration: 500, + }); + } + + slideOut() { + this.scene.tweens.add({ + targets: this, + y: { start: this.modalConfigs.animTo, to: this.modalConfigs.animStart }, + ease: 'Expo.Out', + repeat: 0, + duration: 500, + }); + } + + updateEnemyHp() { + if (this.gameObject instanceof Unit) { + this.infoText_1.setText(`${this.gameObject.hp}/${this.gameObject.maxHp}`); + if (this.gameObject.hp === 0) this.slideOut(); + } + } +} diff --git a/frontend/src/scripts/components/interface/GameRoundStats.ts b/frontend/src/scripts/components/interface/GameRoundStats.ts new file mode 100644 index 0000000..fe2e3a9 --- /dev/null +++ b/frontend/src/scripts/components/interface/GameRoundStats.ts @@ -0,0 +1,17 @@ +// состояние текущего раунда игры (каждой конкретной игровой сессии каждого конкретного уровня в момент запуска) + +export default class GameObjStats extends Phaser.Scene { + text: Phaser.GameObjects.Text; + state: any; + + constructor(scene, state) { + super(scene); + this.state = state; + this.text = scene.add.text(50, 150, '', { + fontFamily: 'Dimbo', + fontSize: '34px', + color: 'black', + weight: 'bold', + }); + } +} diff --git a/frontend/src/scripts/components/interface/GameStats.ts b/frontend/src/scripts/components/interface/GameStats.ts new file mode 100644 index 0000000..9bae333 --- /dev/null +++ b/frontend/src/scripts/components/interface/GameStats.ts @@ -0,0 +1,93 @@ +import langConfig from "../../layouts/langConfig"; + +export default class GameStats extends Phaser.GameObjects.Container { + livesInfo: Phaser.GameObjects.Text + + goldsInfo: Phaser.GameObjects.Text + + wavesInfo: Phaser.GameObjects.Text + + wavesCount: number; + + constructor(scene: Phaser.Scene) { + super(scene); + scene.add.existing(this); + + this.setSize(+scene.sys.game.config.width / 6, 120); + this.setPosition(20, 20); + + this.depth = 1000; + + this.drawContainers(); + this.generate(); + + scene.tweens.add({ + targets: this, + y: { start: this.y - this.height, to: this.y }, + ease: 'Cubic.Out', + repeat: 0, + duration: 1000, + }); + } + + drawContainers() { + const borderColor = 0x593517; + const bgColor = 0x42250f; + + const livesContainer = this.scene.add.graphics(); + livesContainer.fillStyle(bgColor); + livesContainer.fillRoundedRect(0, 0, this.width*0.35, 50, 10); + livesContainer.lineStyle(10, borderColor); + livesContainer.strokeRoundedRect(0, 0, this.width*0.35, 50, 10); + livesContainer.setAlpha(0.8); + const heart = this.scene.add.image(0, 0, 'heart-icon').setScale(1.2).setOrigin(0.3, 0.1); + + const goldsContainer = this.scene.add.graphics(); + goldsContainer.fillStyle(bgColor); + goldsContainer.fillRoundedRect(this.width*0.42, 0, this.width*0.58, 50, 10); + goldsContainer.lineStyle(10, borderColor); + goldsContainer.strokeRoundedRect(this.width*0.42, 0, this.width*0.58, 50, 10); + goldsContainer.setAlpha(0.8); + const coins = this.scene.add.image(this.width*0.42, 0, 'coins-icon').setScale(1.2).setOrigin(0.3, 0.1); + + const wavesContainer = this.scene.add.graphics(); + wavesContainer.fillStyle(bgColor); + wavesContainer.fillRoundedRect(0, 70, this.width, 50, 10); + wavesContainer.lineStyle(10, borderColor); + wavesContainer.strokeRoundedRect(0, 70, this.width, 50, 10); + wavesContainer.setAlpha(0.8); + const skull = this.scene.add.image(0, 70, 'wave-icon').setScale(1.2).setOrigin(0.3, 0.1); + + this.add(livesContainer); + this.add(goldsContainer); + this.add(wavesContainer); + this.add(heart); + this.add(coins); + this.add(skull); + } + + generate() { + const styles = { fontFamily: 'Dimbo', fontSize: '40px' }; + this.livesInfo = this.scene.add.text(this.width*0.2, 25, '', styles).setOrigin(0.5); + this.goldsInfo = this.scene.add.text(this.width*0.75, 25, '', styles).setOrigin(0.5); + this.wavesInfo = this.scene.add.text(this.width*0.5, 95, '', styles).setOrigin(0.5); + + this.add(this.livesInfo); + this.add(this.goldsInfo); + this.add(this.wavesInfo); + } + + updateLives(playerLives: number) { + this.livesInfo.setText(playerLives.toString()); + } + + updateGolds(golds: number) { + this.goldsInfo.setText(golds.toString()); + } + + updateWaves(currentWave: number, maxWaves?: number) { + if (maxWaves) this.wavesCount = maxWaves; + const waveText = langConfig[`${window['lang']}`].wave.toUpperCase(); + this.wavesInfo.setText(`${waveText} ${currentWave}/${this.wavesCount}`); + } +} \ No newline at end of file diff --git a/frontend/src/scripts/components/map/Map.ts b/frontend/src/scripts/components/map/Map.ts new file mode 100644 index 0000000..3c8c575 --- /dev/null +++ b/frontend/src/scripts/components/map/Map.ts @@ -0,0 +1,26 @@ +import { MapType } from '../../constants/maps'; +import GameScene from '../scenes/GameScene'; + +export default class Map { + scene: GameScene; + + mapData: MapType; + + map: Phaser.GameObjects.Image; + + width: number; + + height: number; + + constructor(scene: GameScene, mapData: MapType) { + this.scene = scene; + this.mapData = mapData; + this.width = +scene.game.config.width; + this.height = +scene.game.config.height; + } + + create(): void { + this.map = this.scene.add.image(0, 0, `map_${this.scene.levelSettings.level}`).setOrigin(0, 0); + this.map.setDisplaySize(this.width, this.height); + } +} diff --git a/frontend/src/scripts/components/map/MapLevel.ts b/frontend/src/scripts/components/map/MapLevel.ts new file mode 100644 index 0000000..826f666 --- /dev/null +++ b/frontend/src/scripts/components/map/MapLevel.ts @@ -0,0 +1,106 @@ +import Phaser from 'phaser'; +import Map from './Map'; +import { MapType } from '../../constants/maps'; +import getRandomDeviationWay from '../../utils/getRandomDeviationWay'; +import Tower from '../tower/Tower'; +import GameScene from '../scenes/GameScene'; + +export interface MapLevel { + new(scene: Phaser.Scene, mapData: MapType): Map +} + +export class MapLevel extends Map { + curve: Phaser.Curves.Path; + + mapData: MapType; + + startPointX: number; + + startPointY: number; + + finishPointX: number; + + finishPointY: number; + + scalePointsWay: Array; + + scaleCoordinateTowers: Array; + + constructor(scene: GameScene, mapData: MapType) { + super(scene, mapData); + this.mapData = mapData; + this.startPointX = this.mapData.scaleStartPointX; + this.startPointY = this.height / this.mapData.scaleStartPointY; + this.finishPointX = this.width / this.mapData.scaleFinishPointX; + this.finishPointY = this.height / this.mapData.scaleFinishPointY; + this.scalePointsWay = this.mapData.scalePointsWay; + this.scaleCoordinateTowers = this.mapData.scaleCoordinateTowers; + } + + createWay(): Phaser.Curves.Path { + const points: Array = []; + const randomWay = Math.round(Math.random()); + this.scalePointsWay.forEach((scalePoint: any) => { + if (scalePoint instanceof Array) { + this.createPointWay(points, scalePoint[randomWay]); + } else { + this.createPointWay(points, scalePoint); + } + }); + this.curve = new Phaser.Curves.Path(this.startPointX, this.startPointY); + this.curve.splineTo(points); + return this.curve; + } + + addTowers(): Tower[] { + const towers: Tower[] = []; + this.mapData.scaleCoordinateTowers.forEach((coordinate) => { + const tower = this.createTower(coordinate); + tower.placeField(); + towers.push(tower); + tower.on('pointerdown', () => tower.choiceTower(), this); + }); + return towers; + } + + createTower(coordinate: object): Tower { + const scaleCoordinateX: number = Object.values(coordinate)[0]; + const scaleCoordinateY: number = Object.values(coordinate)[1]; + const x = this.width / scaleCoordinateX; + const y = this.height / scaleCoordinateY; + const tower = new Tower(this.scene, x, y, this.mapData); + return tower; + } + + createPointWay(points: Array, scalePoint: object): void { + const scaleX: number = Object.values(scalePoint)[0]; + const scaleY: number = Object.values(scalePoint)[1]; + const pointX: number = this.getRandomPointX(scaleX); + const pointY: number = this.getRandomPointY(scaleY); + points.push(new Phaser.Math.Vector2(pointX, pointY)); + } + + getRandomPointX(scale: number): number { + return (this.width / scale) + getRandomDeviationWay(); + } + + getRandomPointY(scale: number): number { + return (this.height / scale) + getRandomDeviationWay(); + } + + getStartPointX(): number { + return this.startPointX; + } + + getStartPointY(): number { + return this.startPointY; + } + + getFinishPointX(): number { + return this.finishPointX; + } + + getFinishPointY(): number { + return this.finishPointY; + } +} diff --git a/frontend/src/scripts/components/missile/Missile.ts b/frontend/src/scripts/components/missile/Missile.ts new file mode 100644 index 0000000..fe44e98 --- /dev/null +++ b/frontend/src/scripts/components/missile/Missile.ts @@ -0,0 +1,43 @@ +import 'phaser'; + +export default class Missile extends Phaser.GameObjects.Image { + dx: number; + + dy: number; + + lifespan: number; + + speed: number; + + constructor(scene: Phaser.Scene, x: number, y: number, type: string) { + super(scene, 0, 0, type); + this.dx = 0; + this.dy = 0; + this.lifespan = 0; + this.speed = Phaser.Math.GetSpeed(600, 1); + } + + fire(x: number, y: number, angle: number): void { + this.setActive(true); + this.setVisible(true); + this.setPosition(x, y); + this.setRotation(angle); + + this.dx = Math.cos(angle); + this.dy = Math.sin(angle); + + this.lifespan = 300; + } + + update(time: number, delta: number) { + this.lifespan -= delta; + + this.x += this.dx * (this.speed * delta); + this.y += this.dy * (this.speed * delta); + + if (this.lifespan < 0) { + this.setActive(false); + this.setVisible(false); + } + } +} diff --git a/frontend/src/scripts/components/missile/MissileArrow.ts b/frontend/src/scripts/components/missile/MissileArrow.ts new file mode 100644 index 0000000..7dce575 --- /dev/null +++ b/frontend/src/scripts/components/missile/MissileArrow.ts @@ -0,0 +1,7 @@ +import Missile from './Missile'; + +export default class MissileArrow extends Missile { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'missile-arrow'); + } +} diff --git a/frontend/src/scripts/components/missile/MissileBomb.ts b/frontend/src/scripts/components/missile/MissileBomb.ts new file mode 100644 index 0000000..3b48f55 --- /dev/null +++ b/frontend/src/scripts/components/missile/MissileBomb.ts @@ -0,0 +1,7 @@ +import Missile from './Missile'; + +export default class MissileBomb extends Missile { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'missile-bomb'); + } +} diff --git a/frontend/src/scripts/components/missile/MissileMagic.ts b/frontend/src/scripts/components/missile/MissileMagic.ts new file mode 100644 index 0000000..ccac7be --- /dev/null +++ b/frontend/src/scripts/components/missile/MissileMagic.ts @@ -0,0 +1,7 @@ +import Missile from './Missile'; + +export default class MissileMagic extends Missile { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'missile-magic'); + } +} diff --git a/frontend/src/scripts/components/modal/CustomModal.ts b/frontend/src/scripts/components/modal/CustomModal.ts new file mode 100644 index 0000000..4fb8641 --- /dev/null +++ b/frontend/src/scripts/components/modal/CustomModal.ts @@ -0,0 +1,65 @@ +import Modal from './Modal'; +import Button from '../button/Button'; + +export default class CustomModal extends Modal { + closeModalBtn: Button; + + ropeLeft: Phaser.GameObjects.Image; + + ropeRight: Phaser.GameObjects.Image; + + constructor(scene: Phaser.Scene, bgTexture: string, title: string) { + super(scene, bgTexture, title); + + this.addRopes(scene); + this.initializeCloseBtn(scene); + + this.add(this.ropeLeft); + this.add(this.ropeRight); + } + + initializeCloseBtn(scene: Phaser.Scene) { + this.closeModalBtn = new Button(scene, 0, 0, 'modal-close-btn'); + const closeBtnCoordinates = [ + this.bgImage.width / 2 - this.closeModalBtn.btnImage.width / 4, + -this.bgImage.height / 2 + this.closeModalBtn.btnImage.height / 4, + ]; + this.closeModalBtn.setPosition(closeBtnCoordinates[0], closeBtnCoordinates[1]); + this.add(this.closeModalBtn); + } + + addRopes(scene: Phaser.Scene) { + const ropeLeftCoordinates = [ + -this.bgImage.width / 3, + -(3 * this.bgImage.height) / 8, + ]; + this.ropeLeft = scene.add + .image(ropeLeftCoordinates[0], ropeLeftCoordinates[1], 'rope-big') + .setOrigin(0, 1); + + const ropeRightX = this.bgImage.width / 3 - this.ropeLeft.width; + this.ropeRight = scene.add + .image(ropeRightX, ropeLeftCoordinates[1], 'rope-big') + .setOrigin(0, 1); + } + + slideIn() { + this.scene.tweens.add({ + targets: this, + y: { start: -this.scene.cameras.main.centerY, to: +this.scene.cameras.main.centerY }, + ease: 'Cubic.Out', + repeat: 0, + duration: 500, + }); + } + + slideOut() { + this.scene.tweens.add({ + targets: this, + y: { start: this.scene.cameras.main.centerY, to: -this.scene.cameras.main.centerY }, + ease: 'Expo.Out', + repeat: 0, + duration: 500, + }); + } +} diff --git a/frontend/src/scripts/components/modal/HotKeysModal.ts b/frontend/src/scripts/components/modal/HotKeysModal.ts new file mode 100644 index 0000000..eb78b2e --- /dev/null +++ b/frontend/src/scripts/components/modal/HotKeysModal.ts @@ -0,0 +1,51 @@ +import CustomModal from './CustomModal'; +import langConfig from '../../layouts/langConfig'; + +export default class HotKeysModal extends CustomModal { + lang: string + + constructor(scene: Phaser.Scene) { + super(scene, 'hotkeys-modal', `${langConfig[`${window['lang']}`].hotkeysTitle}`); + + this.slideOut(); + this.closeModalBtn.setInteractive().on('pointerup', () => this.slideOut()); + + this.generateTexts(); + } + + generateTexts() { + this.lang = window['lang']; + const keysConfigs = langConfig[`${window['lang']}`].hotkeys; + + const xStart = this.bgImage.x / 2 - this.bgImage.width / 2 + 85; + const yStart = this.bgImage.y / 2 - this.bgImage.height / 2 + this.headerBg.height; + for (let i = yStart, k = 0; k < keysConfigs.length; i += 90, k++) { + this.generateHotKeyInfo(xStart, i, keysConfigs[k].key, keysConfigs[k].info); + } + } + + generateHotKeyInfo(x: number, y: number, hotkey: string, text: string) { + // border 0x593517 bg 0x42250F + const keyBg = this.scene.add.graphics(); + keyBg.fillStyle(0x593517); + keyBg.fillRoundedRect(x, y, 240, 80, 10); + keyBg.fillStyle(0x42250f); + keyBg.fillRoundedRect(x + 5, y + 5, 230, 60, 10); + + const infoBg = this.scene.add.graphics(); + infoBg.fillStyle(0x593517); + infoBg.fillRoundedRect(x + 250, y, 860, 80, 10); + infoBg.fillStyle(0x42250f); + infoBg.fillRoundedRect(x + 260, y + 10, 840, 60, 10); + + const keyStyle = { fontFamily: 'Dimbo', fontStyle: 'italic', fontSize: '35px' }; + const keyText = this.scene.add + .text(x + 120, y + 40, `${hotkey} `, keyStyle) + .setOrigin(0.5, 0.5); + + const infoStyle = { fontFamily: 'Dimbo', fontSize: '38px' }; + const infoText = this.scene.add.text(x + 280, y + 40, text, infoStyle).setOrigin(0, 0.5); + + this.add([keyBg, infoBg, keyText, infoText]); + } +} diff --git a/frontend/src/scripts/components/modal/LoseModal.ts b/frontend/src/scripts/components/modal/LoseModal.ts new file mode 100644 index 0000000..e12af5a --- /dev/null +++ b/frontend/src/scripts/components/modal/LoseModal.ts @@ -0,0 +1,70 @@ +import Modal from './Modal'; +import Button from '../button/Button'; +import langConfig from '../../layouts/langConfig'; + +export default class LoseModal extends Modal { + window: Phaser.GameObjects.Image; + + textMessage: Phaser.GameObjects.Text; + + starsImage: Phaser.GameObjects.Image; + + restartBtn: Button + + cancelBtn: Button + + constructor(scene: Phaser.Scene) { + const config = langConfig[`${window['lang']}`]; + const failText = config.failed.toUpperCase(); + super(scene, 'table', failText); + + this.header.setY(this.header.y + 15); + + this.window = scene.add.image(0, 30, 'fail-bg').setOrigin(0.5); + this.add(this.window); + this.starsImage = scene.add.image(0, 0, 'star-grey'); + this.starsImage.setY(-this.window.y-this.starsImage.height/4); + this.add(this.starsImage); + + const styles = { + fontFamily: 'Dimbo', + fontSize: '60px', + color: '#dbc899', + align: 'center', + }; + this.textMessage = scene.add.text(0, 0, config.sorry, styles).setOrigin(0.5, -0.5); + this.textMessage.setWordWrapCallback((text: string) => text.split(/\//)); + this.add(this.textMessage); + + this.initializeButtons(scene); + } + + initializeButtons(scene: Phaser.Scene) { + this.cancelBtn = new Button(scene, 0, 0, 'button-left'); + const cancelBtnCoordinates = [ + -this.bgImage.width / 2 + this.cancelBtn.width, + this.bgImage.height / 2 - this.cancelBtn.width / 4, + ]; + this.cancelBtn.setPosition(cancelBtnCoordinates[0], cancelBtnCoordinates[1]); + + this.restartBtn = new Button(scene, 0, 0, 'button-restart'); + const restartBtnCoordinates = [ + this.bgImage.width / 2 - this.restartBtn.width, + this.bgImage.height / 2 - this.restartBtn.width / 4, + ]; + this.restartBtn.setPosition(restartBtnCoordinates[0], restartBtnCoordinates[1]); + + this.add(this.cancelBtn); + this.add(this.restartBtn); + } + + disappearance() { + this.scene.tweens.add({ + targets: this, + scale: { start: this.scale, to: 0 }, + ease: 'Cubic.Out', + repeat: 0, + duration: 1000, + }); + } +} diff --git a/frontend/src/scripts/components/modal/Modal.ts b/frontend/src/scripts/components/modal/Modal.ts new file mode 100644 index 0000000..05e45ef --- /dev/null +++ b/frontend/src/scripts/components/modal/Modal.ts @@ -0,0 +1,43 @@ +export default class Modal extends Phaser.GameObjects.Container { + bgImage: Phaser.GameObjects.Image; + + header: Phaser.GameObjects.Container; + + headerBg: Phaser.GameObjects.Image; + + headerText: Phaser.GameObjects.Text + + constructor(scene: Phaser.Scene, bgTexture: string, title: string) { + super(scene, scene.cameras.main.centerX, scene.cameras.main.centerY); + scene.add.existing(this); + + this.bgImage = scene.add.image(0,0, bgTexture); + + this.header = scene.add.container(); + this.headerBg = scene.add.image(0, 0, 'header-bg').setOrigin(0.5); + const styles = { + fontFamily: 'Dimbo', + fontSize: '60px', + }; + this.headerText = scene.add.text(0, 0, title, styles).setOrigin(0.5, 0.65); + this.headerText.setShadow(3, 3, '#000'); + this.headerText.setTint(0xFAE90F, 0xFAE90F, 0xF7BB1F, 0xF7BB1F); + + this.init(); + } + + init() { + const headerCoordinates = [ + 0, + -this.bgImage.height / 2 + (2 * this.headerBg.height) / 5, + ]; + this.header.setPosition(headerCoordinates[0], headerCoordinates[1]); + this.header.add(this.headerBg); + this.header.add(this.headerText); + + this.add(this.bgImage); + this.add(this.header); + + this.setSize(this.bgImage.width, this.bgImage.height); + } +} diff --git a/frontend/src/scripts/components/modal/PauseModal.ts b/frontend/src/scripts/components/modal/PauseModal.ts new file mode 100644 index 0000000..e52d67d --- /dev/null +++ b/frontend/src/scripts/components/modal/PauseModal.ts @@ -0,0 +1,87 @@ +import CustomModal from './CustomModal'; +import Button from '../button/Button'; +import AudioSlider from '../interface/AudioSlider'; +import langConfig from '../../layouts/langConfig'; + +export default class PauseModal extends CustomModal { + menuBtn: Button; + + restartBtn: Button; + + resumeBtn: Button; + + options: Phaser.GameObjects.Container; + + constructor(scene: Phaser.Scene) { + super(scene, 'settings-modal-bg', langConfig[`${window['lang']}`].options.toUpperCase()); + + this.initializeButtons(scene); + this.initOptionsContainer(scene); + } + + initializeButtons(scene: Phaser.Scene) { + const menuBtnCoordinates = [ + -this.bgImage.width / 3, + this.bgImage.height / 2, + ]; + this.menuBtn = new Button(scene, menuBtnCoordinates[0], menuBtnCoordinates[1], 'button-menu'); + + const restartBtnCoordinates = [ + 0, + this.bgImage.height / 2, + ]; + this.restartBtn = new Button( + scene, + restartBtnCoordinates[0], + restartBtnCoordinates[1], + 'button-restart', + ); + + const resumeBtnCoordinates = [ + this.bgImage.width / 3, + this.bgImage.height / 2, + ]; + this.resumeBtn = new Button( + scene, + resumeBtnCoordinates[0], + resumeBtnCoordinates[1], + 'button-right', + ); + + this.add(this.restartBtn); + this.add(this.resumeBtn); + this.add(this.menuBtn); + } + + initOptionsContainer(scene: Phaser.Scene) { + this.options = new Phaser.GameObjects.Container( + scene, + 0, + 25, + ); + + const bgImage = scene.add.image(0, 0, 'audio-set-bg'); + this.options.add(bgImage); + this.options.setSize(bgImage.width, bgImage.height); + + const musicSlider = new AudioSlider( + scene, + -bgImage.width * 0.45, + -bgImage.height *0.45, + langConfig[`${window['lang']}`].music, + 'music', + ); + + const soundSlider = new AudioSlider( + scene, + -bgImage.width * 0.45, + 0, + langConfig[`${window['lang']}`].sound, + 'sounds', + ); + + this.options.add(musicSlider); + this.options.add(soundSlider); + this.add(this.options); + } +} diff --git a/frontend/src/scripts/components/modal/StartScreenModal.ts b/frontend/src/scripts/components/modal/StartScreenModal.ts new file mode 100644 index 0000000..7712afc --- /dev/null +++ b/frontend/src/scripts/components/modal/StartScreenModal.ts @@ -0,0 +1,121 @@ +import CustomModal from './CustomModal'; +import Button from '../button/Button'; +import DiffButton from '../button/DiffButton'; +import langConfig from '../../layouts/langConfig'; + +export default class StartScreenModal extends CustomModal { + mapImage: Phaser.GameObjects.Image; + + levelText: Phaser.GameObjects.Container; + + towersNumberText: Phaser.GameObjects.Text; + + level: number; + + difficultyBtn: DiffButton + + difficultyImage: Phaser.GameObjects.Image; + + startBtn: Button + + constructor( + scene: Phaser.Scene, + towersNumber: number, + level: number, + ) { + const lang = window['lang']; + const levelText = langConfig[`${lang}`].level.toUpperCase(); + super(scene, 'start-modal-bg', `${levelText} ${level}`); + + this.drawMapImage(scene, level); + this.addText(scene, level); + this.gameDifficulty(scene, 'easy'); + this.addStartButton(scene); + + const possibleTowers = langConfig[`${lang}`].towersNumber; + this.towersNumberText = scene.add.text( + this.bgImage.width / 5, + this.bgImage.width / 5, + `${possibleTowers}: ${towersNumber}`, + { fontSize: '40px', fontFamily: 'Dimbo', color: '#c0c0c0' }, + ).setOrigin(0.5); + + this.add(this.mapImage); + this.add(this.levelText); + this.add(this.towersNumberText); + } + + drawMapImage(scene: Phaser.Scene, level: number) { + const mapScale = 0.33; + const mapImageCoordinates = [ + -this.bgImage.width / 4, + -this.bgImage.width / 15, + ]; + this.mapImage = scene.add + .image(mapImageCoordinates[0], mapImageCoordinates[1], `map_${level}`) + .setScale(mapScale); + + const graphics = scene.add.graphics(); + graphics.lineStyle(4, 0x000000, 1); + graphics.strokeRect( + mapImageCoordinates[0] - (this.mapImage.width / 2) * mapScale, + mapImageCoordinates[1] - (this.mapImage.height / 2) * mapScale, + this.mapImage.width * mapScale, + this.mapImage.height * mapScale, + ); + graphics.lineStyle(10, 0xc0c0c0, 1); + graphics.strokeRoundedRect( + mapImageCoordinates[0] - (this.mapImage.width / 2) * mapScale - 7, + mapImageCoordinates[1] - (this.mapImage.height / 2) * mapScale - 7, + this.mapImage.width * mapScale + 14, + this.mapImage.height * mapScale + 14, + 10, + ); + this.add(graphics); + } + + addText(scene: Phaser.Scene, level: number) { + const lang = window['lang']; + const levelText = langConfig[`${lang}`].levelTheme[`${level}`]; + + const levelTextCoordinates = [ + -30, + -this.bgImage.width / 2 + this.mapImage.displayHeight+15, + ]; + this.levelText = new Phaser.GameObjects.Container( + scene, + levelTextCoordinates[0], + levelTextCoordinates[1], + ); + this.levelText.setSize(this.bgImage.width / 2, this.bgImage.height / 2); + const levelInfo = scene.add + .text(0, 0, levelText, { + fontFamily: 'Dimbo', + fontSize: '32px', + align: 'justify', + wordWrap: { width: this.levelText.width }, + }) + .setOrigin(0, 0); + + this.levelText.add(levelInfo); + } + + gameDifficulty(scene: Phaser.Scene, difficulty: string) { + const difficultyImageCoordinates = [ + -this.bgImage.width / 4, + this.bgImage.width / 4, + ]; + this.difficultyBtn = new DiffButton(scene, difficultyImageCoordinates[0], difficultyImageCoordinates[1]); + this.add(this.difficultyBtn); + } + + addStartButton(scene: Phaser.Scene) { + this.startBtn = new Button(scene, 0, 0, 'button-start'); + const startBtnCoordinates = [ + this.bgImage.width / 2 - this.startBtn.btnImage.width, + this.bgImage.height / 2 - this.startBtn.btnImage.height / 5, + ]; + this.startBtn.setPosition(startBtnCoordinates[0], startBtnCoordinates[1]); + this.add(this.startBtn); + } +} diff --git a/frontend/src/scripts/components/modal/WinModal.ts b/frontend/src/scripts/components/modal/WinModal.ts new file mode 100644 index 0000000..5f2c851 --- /dev/null +++ b/frontend/src/scripts/components/modal/WinModal.ts @@ -0,0 +1,70 @@ +import Modal from './Modal'; +import Button from '../button/Button'; +import langConfig from '../../layouts/langConfig'; + +export default class WinModal extends Modal { + window: Phaser.GameObjects.Image; + + textMessage: Phaser.GameObjects.Text; + + continueBtn: Button + + restartBtn: Button + + starsImage: Phaser.GameObjects.Image; + + constructor(scene: Phaser.Scene, starsNumber: 1 | 2 | 3) { + const config = langConfig[`${window['lang']}`]; + const winText = config.win.toUpperCase(); + super(scene, 'table', winText); + + this.header.setY(this.header.y + 15); + + this.window = scene.add.image(0, 30, 'fail-bg').setOrigin(0.5); + this.add(this.window); + this.starsImage = scene.add.image(0, 0, `star-${starsNumber}`); + this.starsImage.setY(-this.window.y-this.starsImage.height/4); + this.add(this.starsImage); + + const styles = { + fontFamily: 'Dimbo', + fontSize: '60px', + color: '#dbc899', + align: 'center' + }; + this.textMessage = scene.add.text(0, 0, config.congrats, styles).setOrigin(0.5, -0.5); + this.textMessage.setWordWrapCallback((text: string) => text.split(/\//)); + this.add(this.textMessage); + + this.initializeButtons(scene); + } + + initializeButtons(scene: Phaser.Scene) { + this.restartBtn = new Button(scene, 0, 0, 'button-restart'); + const restartBtnCoordinates = [ + -this.bgImage.width / 2 + this.restartBtn.width, + this.bgImage.height / 2 - this.restartBtn.width / 4, + ]; + this.restartBtn.setPosition(restartBtnCoordinates[0], restartBtnCoordinates[1]); + + this.continueBtn = new Button(scene, 0, 0, 'button-right'); + const continueBtnCoordinates = [ + this.bgImage.width / 2 - this.continueBtn.width, + this.bgImage.height / 2 - this.continueBtn.width / 4, + ]; + this.continueBtn.setPosition(continueBtnCoordinates[0], continueBtnCoordinates[1]); + + this.add(this.restartBtn); + this.add(this.continueBtn); + } + + disappearance() { + this.scene.tweens.add({ + targets: this, + scale: { start: this.scale, to: 0 }, + ease: 'Cubic.Out', + repeat: 0, + duration: 1000, + }); + } +} diff --git a/frontend/src/scripts/components/scenes/GameScene.ts b/frontend/src/scripts/components/scenes/GameScene.ts new file mode 100644 index 0000000..51ed1ab --- /dev/null +++ b/frontend/src/scripts/components/scenes/GameScene.ts @@ -0,0 +1,446 @@ +import 'phaser'; +import { MapLevel } from '../map/MapLevel'; +import EnemyFactory from '../unit/EnemyFactory'; +import { levelsConfig } from '../../constants/constants'; +import sendDataToBackend from '../../achievements/utils/backend'; + +import Tower from '../tower/Tower'; +import { GameObjects } from 'phaser'; + +import GameObjStats from '../interface/GameObjStats'; +import Button from '../button/Button'; +import Gate from '../Gate'; +import createAnims from '../unit/createAnims'; +import GameStats from '../interface/GameStats'; +import LevelSettings from '../../LevelSettings'; +import { PlayerStatsManager } from '../stats/PlayerStats'; +import WaveButton from '../button/WaveButton'; +import waveBtnConfigs from '../../constants/waveBtnConfigs'; + +export default class GameScene extends Phaser.Scene { + map: MapLevel; + + firstPointX: number; + + firstPointY: number; + + gatePointX: number; + + gatePointY: number; + + pointX: number; + + pointY: number; + + gate: Gate; + + fakeGate: Gate; + + waveBtn: WaveButton; + + waveBtnClone: WaveButton; + + gameObjStats: any; + + levelSettings: any; + + towers: Array + + enemiesGroup: Phaser.GameObjects.Group; + + gold: number; + + playerLives: number; + + passedEnemies: GameObjects.Group[]; + + isDefeat: boolean; + + enemiesProducedCounter: number; + + deathCounter: number; + + gameStats: GameStats; + + music:any + + sounds:any + + constructor() { + super('game-scene'); + } + + setScene(data) { + this.levelSettings = new LevelSettings(data.level, data.gameDifficulty); + this.map = new MapLevel(this, this.levelSettings.config.map); + this.passedEnemies = []; + this.firstPointX = this.map.getStartPointX(); + this.firstPointY = this.map.getStartPointY(); + this.gatePointX = this.map.getFinishPointX(); + this.gatePointY = this.map.getFinishPointY(); + + this.isDefeat = false; + this.gold = this.levelSettings.config.startingGold; + this.playerLives = this.calculatePlayersLivesForDifficulty(); + this.gameStats.updateLives(this.playerLives); + this.gameStats.updateGolds(this.gold); + } + + calculatePlayersLivesForDifficulty() { + switch (this.levelSettings.gameDifficulty) { + case 1: + return 20; + case 2: + return 10; + case 3: + return 1; + default: + return 20; + } + } + + onEnemyCrossing(enemy) { + if (!this.passedEnemies.includes(enemy)) { + this.passedEnemies.push(enemy); + this.playerLives -= 1; + this.sounds.loseLife.play(); + this.gameStats.updateLives(this.playerLives); + if (this.gameObjStats.gameObject === enemy) { + this.gameObjStats.slideOut(); + this.gameObjStats.gameObject = null; + } + setTimeout(() => { + // enemy.texture = this.renderer.setBlendMode; + enemy.destroy(); + }, 3000); + if (this.playerLives <= 0) { + this.defeat(); + } + } + } + + defeat() { + this.sound.stopAll(); + this.sounds.defeat.play(); + this.isDefeat = true; + this.updateGameStatsInLocalStorage('lose'); + + this.scene.pause(); + this.scene.moveAbove('game-scene', 'lose-scene'); + this.scene.launch('lose-scene'); + sendDataToBackend(); + } + + win() { + this.sound.stopAll(); + this.sounds.win.play(); + this.updateGameStatsInLocalStorage('win'); + this.scene.pause(); + this.scene.moveAbove('game-scene', 'win-scene'); + this.scene.launch('win-scene', { starsNumber: this.calculateLevelStars() }); + + sendDataToBackend(); + } + + calculateLevelStars() { + const playerLivesPercent = this.playerLives * 100 / this.calculatePlayersLivesForDifficulty(); + if (playerLivesPercent == 100) { + return 3; + } else if (playerLivesPercent >= 50) { + return 2; + } + return 1; + } + + updateGameStatsInLocalStorage(result = 'playing') { + const data = { + level: this.levelSettings.level, + levelResult: result, + }; + if (this.levelSettings.gameDifficulty === 3) { + data['ironModeProgress'] = result == 'win' ? this.calculateLevelStars() : 0; + console.log(data['ironModeProgress']); + } else { + data['gameProgress'] = result == 'win' ? this.calculateLevelStars() : 0; + } + const playerStatsManager = new PlayerStatsManager(); + playerStatsManager.saveToLocalStorage(data); + } + + produceWaveEnemies(factory: EnemyFactory, currentWave: number): number { + this.gameStats.updateWaves(currentWave); + let enemiesProduced: number = 0; + let currentWaveEnemies: { string, number } = levelsConfig[`level_${this.levelSettings.level}`].waves[`wave_${currentWave}`].enemies; + for (const [enemyType, enemiesNumber] of Object.entries(currentWaveEnemies)) { + for (let i = 0; i < enemiesNumber; i++) { + const enemy = factory.create(enemyType, this.map.createWay()); + const delay = i * 600; + enemy.startFollow({ delay: delay, duration: enemy.moveSpeed, rotateToPath: true }); + this.physics.add.existing(enemy); + this.physics.add.overlap(enemy, this.gate, this.onEnemyCrossing, undefined, this); + this.enemiesGroup.add(enemy); + } + enemiesProduced += enemiesNumber; + } + return enemiesProduced; + } + + createWinTimerChecker() { + this.time.addEvent({ + delay: 1000, + callback: () => { + if (this.scene.scene.registry.list['deathCounter'] === this.enemiesProducedCounter - this.passedEnemies.length) { + this.win(); + } + }, + loop: true, + callbackScope: this, + }); + } + + createWaveTimer(factory: EnemyFactory, wavesCount: number) { + let currentWave = 1; + this.time.addEvent({ + delay: 20000, + callback: () => { + currentWave += 1; + if (currentWave <= wavesCount) { + this.enemiesProducedCounter += this.produceWaveEnemies(factory, currentWave); + } + if (currentWave === wavesCount) { + this.createWinTimerChecker(); + } + }, + repeat: wavesCount - 1, + callbackScope: this, + }); + } + + startBattle() { + this.sound.stopAll(); + const factory = new EnemyFactory(this, this.firstPointX, this.firstPointY); + this.enemiesProducedCounter = 0; + this.enemiesProducedCounter += this.produceWaveEnemies(factory, 1); + const wavesCount = Object.keys(levelsConfig[`level_${this.levelSettings.level}`].waves).length; + this.gameStats.updateWaves(1, wavesCount); + this.createWaveTimer(factory, wavesCount); + } + + createWaveBtn(data) { + this.pointY = this.firstPointY + waveBtnConfigs[data.level].startPointY; + this.pointX = this.firstPointX + waveBtnConfigs[data.level].startPointX; + const path = new Phaser.Curves.Path(); + path.add(new Phaser.Curves.Line([ + this.pointX, + this.pointY, + this.pointX + waveBtnConfigs[data.level].endPointX, + this.pointY + waveBtnConfigs[data.level].endPointY, + ])); + this.waveBtn = this.add.follower(path, this.pointX, this.pointY, 'waveButton'); + if (data.level === 2) { + this.waveBtnClone = this.add.follower(path, this.pointX - 90, this.pointY - 900, 'waveButton'); + this.waveBtnClone.startFollow({ + positionOnPath: false, + duration: 500, + yoyo: true, + repeat: -1, + ease: 'Ease', + }); + this.waveBtnClone.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + if (this.scene.isPaused()) { + return; + } + this.startBattle(); + this.scene.scene.tweens.add({ + targets: [this.waveBtn, this.waveBtnClone], + scale: 0, + ease: 'Linear', + duration: 300, + }); + setTimeout(() => { + this.waveBtn.destroy(); + this.waveBtnClone.destroy(); + }, 310); + this.sounds.startBattle.play(); + // this.sound.play('level-1-attack', { loop: true }); + this.music.levelAttack.play(); + }); + } + + this.waveBtn.rotation -= waveBtnConfigs[data.level].rotation; + this.waveBtn.startFollow({ + positionOnPath: true, + duration: 500, + yoyo: true, + repeat: -1, + ease: 'Ease', + }); + this.waveBtn.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + if (this.scene.isPaused()) { + return; + } + this.startBattle(); + if (this.waveBtnClone) { + this.scene.scene.tweens.add({ + targets: this.waveBtnClone, + scale: 0, + ease: 'Linear', + duration: 300, + }); + setTimeout(() => { + this.waveBtnClone.destroy(); + }, 310); + } + + this.scene.scene.tweens.add({ + targets: this.waveBtn, + scale: 0, + ease: 'Linear', + duration: 300, + }); + setTimeout(() => { + this.waveBtn.destroy(); + }, 310); + this.sounds.startBattle.play(); + this.music.levelAttack.play(); + }); + // hot key для начала волны + this.input.keyboard.on('keyup-N', (event) => { + if (this.scene.isPaused()) { + return; + } + this.startBattle(); + if (this.waveBtnClone) { + this.scene.scene.tweens.add({ + targets: this.waveBtnClone, + scale: 0, + ease: 'Linear', + duration: 300, + }); + setTimeout(() => { + this.waveBtnClone.destroy(); + }, 310); + } + + this.scene.scene.tweens.add({ + targets: this.waveBtn, + scale: 0, + ease: 'Linear', + duration: 300, + }); + setTimeout(() => { + this.waveBtn.destroy(); + }, 310); + this.sounds.startBattle.play(); + this.music.levelAttack.play(); + }); + } + + soundsManager() { + this.sound.stopByKey('main-theme'); + + this.music = { + levelTheme: this.sound.add('level-1', { loop: true }), + levelAttack: this.sound.add('level-1-attack', { loop: true }), + }; + this.sounds = { + defeat: this.sound.add('defeat'), + win: this.sound.add('win'), + loseLife: this.sound.add('lose-life'), + startBattle: this.sound.add('start-battle'), + wizardBlackDie: this.sound.add('wizardBlack-die'), + littleOrcDie: this.sound.add('littleOrc-die'), + scorpioDie: this.sound.add('scorpio-die'), + levendorDie: this.sound.add('levendor-die'), + towerChoice: this.sound.add('tower-choice'), + towerSell: this.sound.add('tower-sell'), + towerBuilding: this.sound.add('tower-building'), + missileArrow: this.sound.add('missile-arrow'), + missileMagic: this.sound.add('missile-magic'), + missileBomb: this.sound.add('missile-bomb'), + }; + + this.music.levelTheme.play(); + } + + create(data: any): void { + this.soundsManager(); + this.cameras.main.fadeIn(750, 0, 0, 0); + this.scene.scene.registry.set('deathCounter', 0); + this.gameStats = new GameStats(this); + this.setScene(data); + this.map.create(); + this.towers = this.map.addTowers(); + this.enemiesGroup = this.physics.add.group(); + createAnims(this); + this.createGate(); + this.createWaveBtn(data); + + // добавляем динамические статы на страницу + this.gameObjStats = new GameObjStats(this); + this.input.on('gameobjectdown', (pointer, gameObject, event) => { + this.gameObjStats.updateStats(gameObject); + }); + + const sceneCenter = [this.cameras.main.centerX, this.cameras.main.centerY]; + + const pauseButton = new Button(this, 0, 0, 'pause-btn'); + const pauseBtnCoordinates = [ + sceneCenter[0] * 2 - pauseButton.width / 2, + pauseButton.height / 2, + ]; + pauseButton.setPosition(pauseBtnCoordinates[0], pauseBtnCoordinates[1]); + pauseButton.setInteractive().on('pointerup', () => { + this.sound.pauseAll(); + this.scene.pause(); + this.scene.moveAbove('game-scene', 'pause-scene'); + this.scene.run('pause-scene'); + }); + + this.input.keyboard.on('keydown-SPACE', (event) => { + if(event.ctrlKey) { + this.sound.pauseAll(); + this.scene.pause(); + this.scene.moveAbove('game-scene', 'pause-scene'); + this.scene.run('pause-scene'); + } + }); + + // const loseBtn = new Button(this, pauseBtnCoordinates[0] * 0.9, pauseBtnCoordinates[1], 'pause-btn'); + // loseBtn.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + // if (this.scene.isPaused()) return; + // this.defeat(); + // }); + + // const winBtn = new Button(this, pauseBtnCoordinates[0] * 0.8, pauseBtnCoordinates[1], 'pause-btn'); + // winBtn.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + // if (this.scene.isPaused()) return; + // this.win() + // }); + + // устанавливает взаимодействие пуль и мобов + this.towers.forEach((tower: Tower) => { + tower.setEnemies(this.enemiesGroup); + }); + + const gateGroup = this.physics.add.existing(this.gate); + } + + createGate() { + this.gate = new Gate(this, this.gatePointX + 60, this.gatePointY + 60, 'gate').setScale(0.5); + this.fakeGate = new Gate(this, this.gatePointX - 55, this.gatePointY, 'gate').setScale(0.5); + this.fakeGate.alpha = 0.6; + this.gate.alpha = 0; + } + + update(time) { + this.fakeGate.rotation += 0.003; + this.towers.forEach((tower: Tower) => { + tower.update(time); + tower.setGold(this.gold); + this.gold = tower.getGold(); + }); + this.gameStats.updateGolds(this.gold); + this.gameObjStats.updateEnemyHp(); + } +} diff --git a/frontend/src/scripts/components/scenes/LevelsScene.ts b/frontend/src/scripts/components/scenes/LevelsScene.ts new file mode 100644 index 0000000..9a605db --- /dev/null +++ b/frontend/src/scripts/components/scenes/LevelsScene.ts @@ -0,0 +1,111 @@ +import { startApp } from '../../App'; +import createStartPage from '../../auth/utils/create.start'; +import LevelButton from '../../components/button/LevelButton'; +import { KEY_ID, KEY_TOKEN } from '../../constants/constants'; +import Button from '../button/Button'; +import HotKeysModal from '../modal/HotKeysModal'; + +export default class LevelsScene extends Phaser.Scene { + cancelBtn: Button + + helpBtn: Button + + hotKeysModal: HotKeysModal + + sounds: { [key: string]: Phaser.Sound.BaseSound | any } = {}; + + constructor() { + super({ key: 'LevelsScene' }); + } + + createSounds() { + this.sounds = { + mainTheme: this.sound.add('main-theme', { + loop: true, + volume: 1, + }), + levelTheme: this.sound.add('level-1', { + loop: true, + volume: 1, + }), + levelAttack: this.sound.add('level-1-attack', { + loop: true, + volume: 1, + }), + + win: this.sound.add('win', { + volume: 1, + }), + defeat: this.sound.add('defeat', { + volume: 1, + }), + }; + + this.sounds.mainTheme.setVolume(0.5); + + if (!this.sounds.mainTheme.isPlaying) { + this.sounds.mainTheme.play(); + } + } + + create() { + this.createSounds(); + this.cameras.main.fadeIn(750, 0, 0, 0); + this.add.image(0, 0, 'levelsMap').setOrigin(0, 0); + + new LevelButton(this, 500, 300, 'level1Button', 1).setAlpha(0.8); + new LevelButton(this, 500, 520, 'level2Button', 2).setAlpha(0.8); + new LevelButton(this, 980, 590, 'level3Button', 3).setAlpha(0.8); + + this.events.on('wake', () => { + this.cameras.main.fadeIn(600, 0, 0, 0); + if (!this.sounds.mainTheme.isPlaying) { + this.sounds.mainTheme.play(); + } + }); + + this.cancelBtn = new Button(this, 50, 50, 'modal-close-btn'); + this.cancelBtn.setInteractive().on('pointerup', () => { + this.cancel(); + }); + + this.input.keyboard.on('keydown-Q', (event) => { + if (event.ctrlKey) { + this.cancel(); + } + }); + + this.helpBtn = new Button(this, 50, 50, 'help-btn'); + this.helpBtn.setX(this.cameras.main.centerX * 2 - 50); + this.hotKeysModal = new HotKeysModal(this); + this.helpBtn.setInteractive().on('pointerup', () => { + if (window['lang'] === this.hotKeysModal.lang) { + this.hotKeysModal.slideIn(); + } else { + this.hotKeysModal = new HotKeysModal(this); + this.hotKeysModal.slideIn(); + } + }); + + this.input.keyboard.on('keydown-ESC', (event) => { + this.hotKeysModal.slideOut(); + }); + } + + cancel() { + this.cameras.main.fadeOut(500, 0, 0, 0); + const token = localStorage.getItem(KEY_TOKEN); + const id = localStorage.getItem(KEY_ID); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.time.delayedCall(100, () => { + (document.querySelector('body') as HTMLBodyElement).style.height = ''; + (document.querySelector('canvas') as HTMLElement).style.display = 'none'; + this.sound.stopAll(); + this.scene.sleep(); + this.game.loop.sleep(); + createStartPage({ id, token }); + document.querySelector('.logo-start-button')?.addEventListener('click', startApp); + }); + }); + } +} diff --git a/frontend/src/scripts/components/scenes/LoseScene.ts b/frontend/src/scripts/components/scenes/LoseScene.ts new file mode 100644 index 0000000..b71cea6 --- /dev/null +++ b/frontend/src/scripts/components/scenes/LoseScene.ts @@ -0,0 +1,52 @@ +import LoseModal from '../modal/LoseModal'; + +export default class LoseScene extends Phaser.Scene { + constructor() { + super({ key: 'lose-scene' }); + } + + preload() {} + + create() { + const modal = new LoseModal(this); + + this.tweens.add({ + targets: modal, + scale: { start: 0.3, to: 1 }, + ease: 'Elastic.Out', + repeat: 0, + duration: 1000, + }); + + modal.cancelBtn.setInteractive().on('pointerup', () => { + modal.disappearance(); + this.cancel(); + }); + + this.input.keyboard.on('keydown-LEFT', (event) => { + if (event.ctrlKey) { + this.cancel(); + } + }); + + modal.restartBtn.setInteractive().on('pointerup', () => { + modal.disappearance(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + + this.input.keyboard.on('keydown-R', (event) => { + modal.disappearance(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + } + + cancel() { + this.cameras.main.fadeOut(500, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.time.delayedCall(500, () => { + this.scene.stop('game-scene'); + this.scene.start('LevelsScene'); + }); + }); + } +} diff --git a/frontend/src/scripts/components/scenes/PauseScene.ts b/frontend/src/scripts/components/scenes/PauseScene.ts new file mode 100644 index 0000000..8878f91 --- /dev/null +++ b/frontend/src/scripts/components/scenes/PauseScene.ts @@ -0,0 +1,64 @@ +import PauseModal from '../modal/PauseModal'; +import Modal from '../modal/Modal'; + +export default class PauseScene extends Phaser.Scene { + modal: PauseModal; + + constructor() { + super({ key: 'pause-scene' }); + } + + preload() {} + + create() { + this.modal = new PauseModal(this); + this.modal.slideIn(); + + this.events.on('resume', () => { + this.modal.slideIn(); + }); + + this.modal.closeModalBtn.setInteractive().on('pointerup', () => this.cancel()); + + this.input.keyboard.on('keydown-ESC', () => this.cancel()); + + this.modal.menuBtn.setInteractive().on('pointerup', () => this.toMenu()); + + this.input.keyboard.on('keydown-M', (event) => { + if (event.ctrlKey) this.toMenu(); + }); + + this.modal.restartBtn.setInteractive().on('pointerup', () => { + this.modal.slideOut(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + + this.input.keyboard.on('keydown-R', (event) => { + this.modal.slideOut(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + + this.modal.resumeBtn.setInteractive().on('pointerup', () => this.cancel()); + } + + toMenu() { + this.modal.slideOut(); + this.cameras.main.fadeOut(500, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.time.delayedCall(1000, () => { + this.scene.stop('game-scene'); + this.scene.start('LevelsScene'); + }); + }); + } + + cancel() { + this.modal.slideOut(); + setTimeout(() => { + this.sound.resumeAll(); + this.scene.pause(); + this.scene.run('game-scene'); + this.scene.moveBelow('game-scene'); + }, 400); + } +} diff --git a/frontend/src/scripts/components/scenes/WinScene.ts b/frontend/src/scripts/components/scenes/WinScene.ts new file mode 100644 index 0000000..b57b983 --- /dev/null +++ b/frontend/src/scripts/components/scenes/WinScene.ts @@ -0,0 +1,66 @@ +import WinModal from '../modal/WinModal'; +import { + isGreatDefender, + isIronDefender, + isCompleteWin, + isFirstAsterisk, +} from '../../constants/achievements'; + +export default class WinScene extends Phaser.Scene { + starsNumber: any; + + constructor(starsNumber) { + super({ key: 'win-scene' }); + this.starsNumber = starsNumber; + } + + preload() {} + + create(data: any) { + const modal = new WinModal(this, data.starsNumber); + + this.tweens.add({ + targets: modal, + scale: { start: 0.3, to: 1 }, + ease: 'Elastic.Out', + repeat: 0, + duration: 1000, + }); + + isGreatDefender(this); + isIronDefender(this); + isCompleteWin(this); + isFirstAsterisk(this); + + modal.continueBtn.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + modal.disappearance(); + this.continue(); + }); + + this.input.keyboard.on('keydown-RIGHT', (event) => { + if (event.ctrlKey) { + this.continue(); + } + }); + + modal.restartBtn.setInteractive().on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => { + modal.disappearance(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + + this.input.keyboard.on('keydown-R', (event) => { + modal.disappearance(); + this.time.delayedCall(300, () => this.scene.start('game-scene')); + }); + } + + continue() { + this.cameras.main.fadeOut(500, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.time.delayedCall(500, () => { + this.scene.start('LevelsScene'); + this.scene.stop('game-scene'); + }); + }); + } +} diff --git a/frontend/src/scripts/components/scenes/preloadScene.ts b/frontend/src/scripts/components/scenes/preloadScene.ts new file mode 100644 index 0000000..8ca878b --- /dev/null +++ b/frontend/src/scripts/components/scenes/preloadScene.ts @@ -0,0 +1,366 @@ +import { map1, map2, map3 } from '../../constants/maps'; +import langConfig from '../../layouts/langConfig' + +interface BarConfigs { + containerCoordinates: number[], + containerSizes: number[], + containerBorderRadius: number, + barCoordinates: number[], + barSizes: number[], + barBorderRadius: number, +} +export default class PreloadScene extends Phaser.Scene { + barContainer: Phaser.GameObjects.Graphics + + progressBar: Phaser.GameObjects.Graphics + + loaderText: Phaser.GameObjects.Text + + constructor() { + super({ key: 'PreloadScene' }); + } + + async preload() { + this.load.image('kingdom-rush-bg', './assets/auth/kingdom-rush.png'); + + this.barContainer = this.add.graphics(); + this.progressBar = this.add.graphics(); + this.preloader() + + // towers + + this.load.spritesheet('circle', './assets/towers/circle.png', { + frameWidth: 200, + frameHeight: 200, + }); + + this.load.spritesheet('sale', './assets/towers/sale.png', { + frameWidth: 55, + frameHeight: 55, + }); + + this.load.spritesheet('arrow', './assets/towers/arrow.png', { + frameWidth: 75, + frameHeight: 75, + }); + + this.load.spritesheet('bomb', './assets/towers/bomb.png', { + frameWidth: 75, + frameHeight: 75, + }); + + this.load.spritesheet('magic', './assets/towers/magic.png', { + frameWidth: 75, + frameHeight: 75, + }); + + this.load.spritesheet('close_tower_button', './assets/towers/close_button_tower.png', { + frameWidth: 75, + frameHeight: 75, + }); + + this.load.spritesheet('tower', './assets/towers/tower.png', { + frameWidth: 120, + frameHeight: 80, + }); + + this.load.spritesheet('missile-arrow', './assets/towers/missile-arrow.png', { + frameWidth: 30, + frameHeight: 10, + }); + + this.load.spritesheet('missile-magic', './assets/towers/missile-magic.png', { + frameWidth: 30, + frameHeight: 30, + }); + + this.load.spritesheet('missile-bomb', './assets/towers/missile-bomb.png', { + frameWidth: 30, + frameHeight: 30, + }); + + //enemies + this.load.spritesheet('scorpio', './assets/sprites/scorpio_walk.png', { + frameWidth: 212, + frameHeight: 246, + }); + + this.load.spritesheet('scorpio_die', './assets/sprites/scorpio_die.png', { + frameWidth: 212, + frameHeight: 246, + }); + + this.load.spritesheet('scorpio_hurt', './assets/sprites/scorpio_hurt.png', { + frameWidth: 212, + frameHeight: 246, + }); + + this.load.spritesheet('wizardBlack', './assets/sprites/wizard-black_walk.png', { + frameWidth: 388, + frameHeight: 338, + }); + + this.load.spritesheet('wizardBlack_die', './assets/sprites/wizard-black_die.png', { + frameWidth: 388, + frameHeight: 338, + }); + + this.load.spritesheet('wizardBlack_hurt', './assets/sprites/wizard-black_hurt.png', { + frameWidth: 388, + frameHeight: 338, + }); + + this.load.spritesheet('littleOrc', './assets/sprites/little-orc_walk.png', { + frameWidth: 331, + frameHeight: 299, + }); + + this.load.spritesheet('littleOrc_die', './assets/sprites/little-orc_die.png', { + frameWidth: 331, + frameHeight: 299, + }); + + this.load.spritesheet('littleOrc_hurt', './assets/sprites/little-orc_hurt.png', { + frameWidth: 331, + frameHeight: 299, + }); + + this.load.spritesheet('levendor', './assets/sprites/levendor_walk.png', { + frameWidth: 294, + frameHeight: 275, + }); + + this.load.spritesheet('levendor_die', './assets/sprites/levendor_die.png', { + frameWidth: 304, + frameHeight: 285, + }); + + this.load.spritesheet('levendor_hurt', './assets/sprites/levendor_hurt.png', { + frameWidth: 304, + frameHeight: 285, + }); + + //other + this.load.spritesheet('gate', './assets/imgs/gate-mini.png', { + frameWidth: 300, + frameHeight: 300, + }); + this.load.image('map_1', map1.url); + this.load.image('map_2', map2.url); + this.load.image('map_3', map3.url); + this.load.image('level1Button', './assets/interface/icon_level_1.png'); + this.load.image('level2Button', './assets/interface/icon_level_2.png'); + this.load.image('level3Button', './assets/interface/icon_level_3.png'); + this.load.image('levelsMap', './assets/main-bg.jpg'); + this.load.spritesheet('waveButton', './assets/icons/wave_button.png', { + frameWidth: 168, + frameHeight: 108, + }); + + + // header + this.load.image('header-bg', './assets/modal-headers/header.png'); + // star rewards + this.load.image('star-grey', './assets/interface/star-grey.png'); + this.load.image('star-1', './assets/interface/star-1.png'); + this.load.image('star-2', './assets/interface/star-2.png'); + this.load.image('star-3', './assets/interface/star-3.png'); + // modal backgrounds + this.load.image('start-modal-bg', './assets/modal-bg/start-modal-bg.png'); + this.load.image('settings-modal-bg', './assets/modal-bg/settings-modal-bg.png'); + this.load.image('audio-set-bg', './assets/modal-bg/audio-set-bg.png'); + this.load.image('table', './assets/modal-bg/table.png'); + this.load.image('fail-bg', './assets/modal-bg/fail-bg.png'); + this.load.image('win-bg', './assets/modal-bg/win-bg.png'); + this.load.image('hotkeys-modal', './assets/modal-bg/hotkeys-modal.png'); + // ropes + this.load.image('rope-small', './assets/interface/rope_small.png'); + this.load.image('rope-big', './assets/interface/rope_big.png'); + // buttons + this.load.image('modal-close-btn', './assets/interface/button_close.png'); + this.load.image('button-start', './assets/interface/button_start.png'); + this.load.image('button-menu', './assets/interface/button_menu.png'); + this.load.image('button-restart', './assets/interface/button_restart.png'); + this.load.image('button-right', './assets/interface/button_right.png'); + this.load.image('button-left', './assets/interface/button_left.png'); + this.load.image('pause-btn', './assets/interface/button_pause.png'); + this.load.image('help-btn', './assets/interface/help.png'); + // icons + this.load.image('armor-icon', './assets/icons/armor.png'); + this.load.image('arrow-icon', './assets/icons/arrows.png'); + this.load.image('bomb-icon', './assets/icons/bomb.png'); + this.load.image('damage-icon', './assets/icons/damage.png'); + this.load.image('heart-icon', './assets/icons/heart.png'); + this.load.image('magic-icon', './assets/icons/magic.png'); + this.load.image('shoes-icon', './assets/icons/shoes.png'); + this.load.image('hour-glass-icon', './assets/icons/hour-glass.png'); + this.load.image('target-icon', './assets/icons/target.png'); + this.load.image('coins-icon', './assets/icons/coins.png'); + this.load.image('wave-icon', './assets/icons/skull.png'); + this.load.image('easy-btn-bg', './assets/interface/easy-btn-bg.png'); + this.load.image('normal-btn-bg', './assets/interface/normal-btn-bg.png'); + this.load.image('hard-btn-bg', './assets/interface/hard-btn-bg.png'); + this.load.image('plus', './assets/interface/button_plus.png'); + this.load.image('minus', './assets/interface/button_minus.png'); + this.load.image('on', './assets/sign/onNoText.png'); + this.load.image('off', './assets/sign/offNoText.png'); + + //ahievements icons + this.load.spritesheet('icon-builder', './assets/achievements/builder.png', { + frameWidth: 176, + frameHeight: 176, + }); + this.load.spritesheet('icon-complete_win', './assets/achievements/complete_win.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-first_asterisk', './assets/achievements/first_asterisk.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-first_blood', './assets/achievements/first_blood.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-great_defender', './assets/achievements/great_defender.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-iron_defender', './assets/achievements/iron_defender.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-killer', './assets/achievements/killer.png', { + frameWidth: 175, + frameHeight: 176, + }); + this.load.spritesheet('icon-seller', './assets/achievements/seller.png', { + frameWidth: 176, + frameHeight: 176, + }); + this.load.spritesheet('achievementPopup', './assets/achievements/achievement_popup_3.png', { + frameWidth: 617, + frameHeight: 256, + }); + + this.load.image('slider-bar-bg', './assets/interface/slider-bar-bg.png'); + + //sounds + this.load.audio('main-theme', './assets/sounds/themes/Main_Theme.mp3'); + this.load.audio('level-1', './assets/sounds/themes/level_1.mp3'); + this.load.audio('level-1-attack', './assets/sounds/themes/Level_Under_Attack.mp3'); + this.load.audio('start-battle', './assets/sounds/waveIncoming.wav'); + this.load.audio('win', './assets/sounds/levelCompleted.wav'); + this.load.audio('defeat', './assets/sounds/levelFailed.wav'); + this.load.audio('lose-life', './assets/sounds/loseLife.wav'); + + this.load.audio('wizardBlack-die', './assets/sounds/enemy_wizard_die.wav'); + this.load.audio('littleOrc-die', './assets/sounds/enemy_orc_die.wav'); + this.load.audio('scorpio-die', './assets/sounds/enemy_scorpio_die.wav'); + this.load.audio('levendor-die', './assets/sounds/enemy_levendor_die.wav'); + + this.load.audio('tower-sell', './assets/sounds/tower_sell.wav'); + this.load.audio('tower-building', './assets/sounds/tower_building.wav'); + this.load.audio('tower-choice', './assets/sounds/GUI_MouseOverTowerIcon.wav'); + this.load.audio('missile-arrow', './assets/sounds/tower_arrow_attack.wav'); + this.load.audio('missile-bomb', './assets/sounds/tower_bomb_attack.wav'); + this.load.audio('missile-magic', './assets/sounds/tower_wizard_attack.wav'); + + this.load.audio('achievement-unlock', './assets/sounds/achievement_unlock.wav'); + } + + create() { + this.add.text(20, 20, 'Loading game...', { fontFamily: 'Dimbo' }); + this.scene.start('LevelsScene'); + // this.scene.start('game-scene', { level: 1, difficulty: 1 }); + } + + preloader() { + const containerCoordinates = [this.cameras.main.centerX / 4, this.cameras.main.centerY * 1.7] + const barCoordinates = [containerCoordinates[0] + 10, containerCoordinates[1] + 10] + const containerSizes = [this.cameras.main.centerX * 1.5, 60] + const barSizes = [containerSizes[0] - 20, containerSizes[1] - 20] + // чтобы каждый раз не считать ... + const barConfig: BarConfigs = { + containerCoordinates, + containerSizes, + containerBorderRadius: containerSizes[1] / 2, + barCoordinates, + barSizes, + barBorderRadius: barSizes[1] / 2, + } + + const loading = langConfig[`${window['lang']}`].loading + this.loaderText = this.add.text( + this.cameras.main.centerX, + containerCoordinates[1] - containerSizes[1], + `${loading} 0%`, + { fontFamily: 'Dimbo', fontSize: '100px', color: '#42250F' }, + ).setOrigin(0.5) + this.add.existing(this.loaderText) + + // border 0x593517 bg 0x42250F + this.barContainer.fillStyle(0x42250F) + this.barContainer.fillRoundedRect( + barConfig.containerCoordinates[0], + barConfig.containerCoordinates[1], + barConfig.containerSizes[0], + barConfig.containerSizes[1], + barConfig.containerBorderRadius, + ) + + this.barContainer.lineStyle(10, 0x593517) + this.barContainer.strokeRoundedRect( + barConfig.containerCoordinates[0], + barConfig.containerCoordinates[1], + barConfig.containerSizes[0], + barConfig.containerSizes[1], + barConfig.containerBorderRadius, + ) + + this.load.on('filecomplete-image-kingdom-rush-bg', () => { + const bg = this.add.image(this.cameras.main.centerX, this.cameras.main.centerY, 'kingdom-rush-bg') + bg.displayHeight = +this.sys.game.config.height + bg.scaleX = bg.scaleY + bg.depth = -10 + }); + + this.load.on('progress', (value: number) => this.drawProgressBar(barConfig, value)); + } + + drawProgressBar(barConfig: BarConfigs, value: number) { + const yellowLight = 0xf4d133 + const yellowDark = 0xde9b26 + const redLight = 0xe65540 + const redDark = 0xc63f31 + + const coefficient = value < 0.03 ? 0.03 : value + let color = coefficient > 0.85 ? redLight : yellowLight; + this.progressBar.clear() + this.progressBar.fillStyle(color) + this.progressBar.fillRoundedRect( + barConfig.barCoordinates[0], + barConfig.barCoordinates[1], + barConfig.barSizes[0] * coefficient, + barConfig.barSizes[1], + barConfig.barBorderRadius, + ) + + color = coefficient > 0.85 ? redDark : yellowDark; + this.progressBar.fillStyle(color) + this.progressBar.fillRoundedRect( + barConfig.barCoordinates[0], + barConfig.barCoordinates[1] + 20, + barConfig.barSizes[0] * coefficient, + barConfig.barSizes[1] / 2, + { + tl: 0, + tr: 0, + bl: barConfig.barBorderRadius, + br: barConfig.barBorderRadius, + }, + ) + const loading = langConfig[`${window['lang']}`].loading + this.loaderText.setText(`${loading} ${(coefficient * 100).toFixed()}%`) + } +} diff --git a/frontend/src/scripts/components/stats/PlayerStats.ts b/frontend/src/scripts/components/stats/PlayerStats.ts new file mode 100644 index 0000000..245aaeb --- /dev/null +++ b/frontend/src/scripts/components/stats/PlayerStats.ts @@ -0,0 +1,97 @@ +type AchievStats = { + firstAsterisk: boolean, + completeVictory: boolean, + firstBlood: boolean, + greatDefender: boolean, + ironDefender: boolean, + killer: boolean, + seller: boolean, + builder: boolean +}; + +export default class PlayerStats { + userId: string; + + gameProgress: number; + + gameLogInCount: number; + + killedEnemies: number; + + builtTowers: number; + + soldTowers: number; + + ironModeProgress: number; + + achievements: AchievStats; + constructor({ + userId, + gameProgress, + gameLogInCount, + killedEnemies, + builtTowers, + soldTowers, + ironModeProgress, + achievements + }) { + this.userId = userId, + this.gameProgress = gameProgress; + this.gameLogInCount = gameLogInCount; + this.killedEnemies = killedEnemies; + this.builtTowers = builtTowers; + this.soldTowers = soldTowers; + this.ironModeProgress = ironModeProgress; + this.achievements = achievements; + } +} + +import { getCurrentPlayerStats, setCurrentPlayerStats } from '../../backend' +import { KEY_TOKEN, KEY_ID, LOCAL_STORAGE_KEY } from "../../constants/constants" + +async function getPlayerStatsFromServer(userId): Promise { + const token = localStorage.getItem(KEY_TOKEN); + const response = await getCurrentPlayerStats({ id: userId, token }); + return response +} + +async function sendPlayerStatsToServer(userId, data): Promise { + const token = localStorage.getItem(KEY_TOKEN); + const response = await setCurrentPlayerStats({ id: userId, token, body: data }); + return response +} + + +class PlayerStatsManager { + + prepopulateLocalStorage(data: object) { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data)) + } + + saveToLocalStorage(data: object) { + let currentPlayerStats = this.getFromLocalStorage(); + if (data['level']) { + const currentLevel = `level_${data['level']}`; + if (currentPlayerStats['gameProgress'][currentLevel] < data['gameProgress']) { + currentPlayerStats['gameProgress'][currentLevel] = data['gameProgress']; + } + if (currentPlayerStats['ironModeProgress'][currentLevel] < data['ironModeProgress']) { + currentPlayerStats['ironModeProgress'][currentLevel] = data['ironModeProgress']; + } + } + if (data['builtTowers']) currentPlayerStats.builtTowers = data['builtTowers']; + if (data['soldTowers']) currentPlayerStats.soldTowers = data['soldTowers']; + if (data['killedEnemies']) currentPlayerStats.killedEnemies = data['killedEnemies']; + if (data['achievements']) currentPlayerStats.achievements = data['achievements']; + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(currentPlayerStats)) + } + + getFromLocalStorage() { + return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || + JSON.stringify({ + builtTowers: 0, soldTowers: 0, killedEnemies: 0, gameLogInCount: 0, achievements: {} +})) + } +} + +export { getPlayerStatsFromServer, sendPlayerStatsToServer, PlayerStatsManager }; diff --git a/frontend/src/scripts/components/stats/TotalStats.ts b/frontend/src/scripts/components/stats/TotalStats.ts new file mode 100644 index 0000000..d133954 --- /dev/null +++ b/frontend/src/scripts/components/stats/TotalStats.ts @@ -0,0 +1,40 @@ +const totalStats = { + PlayerName1: { + gameProgress: 0, + gameLogInCount: 0, + killedEnemies: 0, + builtTowers: 0, + soldTowers: 0, + ironModeProgress: 0, + + achievements: { + firstAsterisk: false, + completeWin: false, + firstBlood: false, + greatDefender: false, + ironDefender: false, + killer: false, + seller: false, + builder: false, + }, + }, + PlayerName2: { + gameProgress: 0, + gameLogInCount: 0, + killedEnemies: 0, + builtTowers: 0, + soldTowers: 0, + ironModeProgress: 0, + + achievements: { + firstAsterisk: false, + completeWin: false, + firstBlood: false, + greatDefender: false, + ironDefender: false, + killer: false, + seller: false, + builder: false, + }, + }, +}; diff --git a/frontend/src/scripts/components/tower/Tower.ts b/frontend/src/scripts/components/tower/Tower.ts new file mode 100644 index 0000000..1f168cd --- /dev/null +++ b/frontend/src/scripts/components/tower/Tower.ts @@ -0,0 +1,423 @@ +import 'phaser'; +import MissileBomb from '../missile/MissileBomb'; +import MissileArrow from '../missile/MissileArrow'; +import MissileMagic from '../missile/MissileMagic'; +import { MapType } from '../../constants/maps'; +import { isBuilder, isSeller } from '../../constants/achievements'; +import { PlayerStatsManager } from '../stats/PlayerStats'; +import Unit from '../unit/Unit'; +import GameObjStats from '../interface/GameRoundStats'; +import langConfig from '../../layouts/langConfig'; +import GameScene from '../scenes/GameScene'; + +export default class Tower extends Phaser.GameObjects.Sprite { + scene: Phaser.Scene; + tower: Phaser.GameObjects.Sprite; + x: number; + y: number; + damage: number; + magicDamage: number; + physicalDamage: number; + missiles: Phaser.GameObjects.Group; + enemies: Array; + isTowerBuilt: boolean; + attackArea: number; + timeShot: number + timeForNextShot: number; + isEnemyAlive: boolean; + archersTowerButton: Phaser.GameObjects.Sprite; + magicTowerButton: Phaser.GameObjects.Sprite; + artilleryTowerButton: Phaser.GameObjects.Sprite; + towerSelectionCircle: Phaser.GameObjects.Sprite; + towersInfo: Array>; + mapData: MapType; + cost: number; + isTowerSold: boolean; + playerGold: number; + costArchersTower: number; + costMagicTower: number; + costArtilleryTower: number; + saleMark: Phaser.GameObjects.Sprite; + canUpdateGold: boolean; + enemyCost: number; + isEnemyDead: boolean; + infoWindow: Phaser.GameObjects.Image; + textInfo: Phaser.GameObjects.Text; + damageArchersTower: number; + damageArtilleryTower: number; + damageMagicTower: number; + speedFireArchersTower: number; + speedFireArtilleryTower: number; + speedFireMagicTower: number; + attackAreaArchersTower: number; + attackAreaArtilleryTower: number; + attackAreaMagicTower: number; + typeArchersTower: string; + typeArtilleryTower: string; + typeMagicTower: string; + graphics: Phaser.GameObjects.Graphics; + closeButton: Phaser.GameObjects.Sprite; + + constructor(scene: Phaser.Scene, positionX: number, positionY: number, mapData: MapType) { + super(scene, positionX, positionY, 'tower'); + this.x = positionX; + this.y = positionY; + this.setInteractive(); + this.isTowerBuilt = false; + this.timeShot = 0; + this.type = ''; + this.isEnemyAlive; + this.timeForNextShot = 1000; + this.mapData = mapData; + this.isTowerSold = false; + this.costArchersTower = 70; + this.costMagicTower = 100; + this.costArtilleryTower = 125; + this.canUpdateGold = false; + this.damageArchersTower = 10; + this.damageArtilleryTower = 35; + this.damageMagicTower = 20; + this.speedFireArchersTower = 1000; + this.speedFireArtilleryTower = 3000; + this.speedFireMagicTower = 1500; + this.attackAreaArchersTower = 280; + this.attackAreaArtilleryTower = 320; + this.attackAreaMagicTower = 300; + this.typeArchersTower = 'archers'; + this.typeArtilleryTower = 'artillery'; + this.typeMagicTower = 'magic'; + } + + public placeField(): void { + this.tower = this.scene.add.sprite(this.x, this.y, 'tower'); + this.tower.setOrigin(0.5, 0.5); + this.tower.setScale(1.2); + if (this.mapData.level === 3) { + this.scene.anims.create({ + key: 'tower_place_desert', + frames: this.scene.anims.generateFrameNumbers('tower', { + start: 4, + end: 4, + }), + frameRate: 0, + repeat: -1, + }); + this.tower.play('tower_place_desert'); + } + } + + public choiceTower(): void { + if (!this.isTowerBuilt) { + this.towerSelectionCircle = this.scene.add.sprite(this.x, this.y, 'circle').setDepth(1); + this.archersTowerButton = this.scene.add.sprite(this.x - 65, this.y - 65, 'arrow').setDepth(1); + this.artilleryTowerButton = this.scene.add.sprite(this.x - 65, this.y + 65, 'bomb').setDepth(1); + this.magicTowerButton = this.scene.add.sprite(this.x + 65, this.y - 65, 'magic').setDepth(1); + this.closeButton = this.scene.add.sprite(this.x + 65, this.y + 65, 'close_tower_button').setDepth(1); + this.towersInfo = [[this.archersTowerButton, this.costArchersTower, this.damageArchersTower, + this.speedFireArchersTower, this.attackAreaArchersTower, this.typeArchersTower], + [this.artilleryTowerButton, this.costArtilleryTower, this.damageArtilleryTower, + this.speedFireArtilleryTower, this.attackAreaArtilleryTower, this.typeArtilleryTower], + [this.magicTowerButton, this.costMagicTower, this.damageMagicTower, + this.speedFireMagicTower, this.attackAreaMagicTower, this.typeMagicTower]]; + this.towersInfo.forEach((towerButton: Array) => { + towerButton[0].setInteractive({ useHandCursor: true }); + }); + this.closeButton.setInteractive({ useHandCursor: true }); + this.towerInformation(); + this.archersTowerButton.on('pointerdown', () => this.placeTowerArrow(), this.archersTowerButton); + this.artilleryTowerButton.on('pointerdown', () => this.placeTowerBomb(), this.artilleryTowerButton); + this.magicTowerButton.on('pointerdown', () => this.placeTowerMagic(), this.magicTowerButton); + this.isTowerBuilt = true; + this.canBuyTower(); + (this.scene as GameScene).sounds.towerChoice.play(); + this.closeButton.on('pointerdown', () => { + this.isTowerBuilt = false; + this.hideChoiceTower(); + (this.scene as GameScene).sounds.towerChoice.play(); + }); + this.closeButton.on('pointermove', () => this.closeButton.setScale(1.2)); + this.closeButton.on('pointerout', () => this.closeButton.setScale(1)); + } + } + + towerInformation(): void { + let hasInfo: boolean = false; + const config = langConfig[`${window['lang']}`]; + this.towersInfo.forEach((towerInfo: Array) => { + towerInfo[0].on('pointermove', () => { + towerInfo[0].setScale(1.2); + if (!hasInfo) { + hasInfo = true; + let infoWindowWidth = +this.scene.game.config.width / 10; + let infoWindowHeight = +this.scene.game.config.height / 12; + this.infoWindow = this.scene.add.image((this.x), (this.y - 150), 'settings-modal-bg').setDepth(1); + this.infoWindow.setDisplaySize(infoWindowWidth, infoWindowHeight); + const styles = { + fontFamily: 'Dimbo', + fontSize: '24px', + color: '#dbc899', + align: 'left', + }; + let text = `${config.tower[towerInfo[5]]}/${config.price}: ${towerInfo[1]}/${config.damage}: ${towerInfo[2]}`; + + this.textInfo = this.scene.add.text((this.x - infoWindowWidth / 2) + 10, + (this.y - infoWindowHeight * 2) + 10, text, styles).setDepth(1); + this.textInfo.setWordWrapCallback((text: string) => text.split(/\//)); + this.graphics = this.scene.add.graphics({ fillStyle: { color: 0x00ff00, alpha: 0.3 } }); + let circle = new Phaser.Geom.Circle(this.x, this.y, towerInfo[4]); + this.graphics.fillCircleShape(circle).setDepth(0); + } + }); + towerInfo[0].on('pointerout', () => { + towerInfo[0].setScale(1); + this.infoWindow.destroy(); + this.textInfo.destroy(); + hasInfo = false; + this.graphics.destroy(); + }); + }); + } + + protected canBuyTower(): void { + this.towersInfo.forEach((towerButton: Array) => { + this.playerGold < towerButton[1] + ? towerButton[0].alpha = 0.5 + : towerButton[0].alpha = 1; + }); + if (this.playerGold < this.costArtilleryTower && this.playerGold < this.costMagicTower + && this.playerGold < this.costArchersTower) { + setTimeout((() => { + this.isTowerBuilt = false; + this.hideChoiceTower(); + }), 2000); + } + } + + canSale(slideOut: CallableFunction, context: object): void { + if (this.isTowerBuilt && !this.saleMark || !this.saleMark.scene) { + this.saleMark = this.scene.add.sprite(this.x, this.y + 75, 'sale'); + this.saleMark.setInteractive({ useHandCursor: true }); + this.saleMark.on('pointermove', () => this.saleMark.setScale(1.2)); + this.saleMark.on('pointerout', () => this.saleMark.setScale(1)); + this.saleMark.on('pointerdown', () => this.sale(slideOut, context)); + setTimeout(() => this.saleMark.destroy(), 2500); + } + } + + protected sale(slideOut, context: object): void { + slideOut.call(context); + const playerStats = new PlayerStatsManager(); + let soldTowers = playerStats.getFromLocalStorage()['soldTowers']; + soldTowers += 1; + isSeller(this.scene); + playerStats.saveToLocalStorage({ 'soldTowers': soldTowers }); + this.canUpdateGold = true; + this.isTowerBuilt = false; + this.type = ''; + this.playerGold += this.cost * 0.8; + this.tower.destroy(); + this.placeField(); + this.saleMark.setVisible(false); + this.saleMark.setActive(false); + this.tower.on('pointerdown', () => this.choiceTower()); + (this.scene as GameScene).sounds.towerSell.play(); + } + + protected placeTowerArrow(): void { + this.cost = this.costArchersTower; + if (this.cost <= this.playerGold) { + const playerStats = new PlayerStatsManager(); + let builtTowers = playerStats.getFromLocalStorage()['builtTowers']; + builtTowers += 1; + isBuilder(this.scene); + playerStats.saveToLocalStorage({ 'builtTowers': builtTowers }); + this.hideChoiceTower(); + this.scene.anims.create({ + key: 'tower_array_anim', + frames: this.scene.anims.generateFrameNumbers('tower', { + start: 1, + end: 1, + }), + frameRate: 0, + repeat: -1, + }); + this.tower.play('tower_array_anim'); + this.createStatsTower(this.damageArchersTower, this.speedFireArchersTower, + this.attackAreaArchersTower, 10); + this.missiles = this.scene.physics.add.group({ classType: MissileArrow, runChildUpdate: true }); + this.isTowerSold = true; + this.type = this.typeArchersTower; + } + } + + protected placeTowerBomb(): void { + this.cost = this.costArtilleryTower; + if (this.cost <= this.playerGold) { + const playerStats = new PlayerStatsManager(); + let builtTowers = playerStats.getFromLocalStorage()['builtTowers']; + builtTowers += 1; + isBuilder(this.scene); + playerStats.saveToLocalStorage({ 'builtTowers': builtTowers }); + this.hideChoiceTower(); + this.scene.anims.create({ + key: 'tower_bomb_anim', + frames: this.scene.anims.generateFrameNumbers('tower', { + start: 3, + end: 3, + }), + frameRate: 0, + repeat: -1, + }); + this.tower.play('tower_bomb_anim'); + this.createStatsTower(this.damageArtilleryTower, this.speedFireArtilleryTower, + this.attackAreaArtilleryTower, 10, 10); + this.missiles = this.scene.physics.add.group({ classType: MissileBomb, runChildUpdate: true }); + this.isTowerSold = true; + this.type = this.typeArtilleryTower; + } + } + + protected placeTowerMagic(): void { + this.cost = this.costMagicTower; + if (this.cost <= this.playerGold) { + const playerStats = new PlayerStatsManager(); + let builtTowers = playerStats.getFromLocalStorage()['builtTowers']; + builtTowers += 1; + isBuilder(this.scene); + playerStats.saveToLocalStorage({ 'builtTowers': builtTowers }); + this.hideChoiceTower(); + this.scene.anims.create({ + key: 'tower_magic_anim', + frames: this.scene.anims.generateFrameNumbers('tower', { + start: 2, + end: 2, + }), + frameRate: 0, + repeat: -1, + }); + this.tower.setScale(1.2); + this.tower.play('tower_magic_anim'); + this.createStatsTower(this.damageMagicTower, this.speedFireMagicTower, + this.attackAreaMagicTower, 0, 10); + this.missiles = this.scene.physics.add.group({ classType: MissileMagic, runChildUpdate: true }); + this.isTowerSold = true; + this.type = this.typeMagicTower; + } + } + + public getGold(): number { + if (this.isTowerSold) { + this.isTowerSold = false; + (this.scene as GameScene).sounds.towerBuilding.play(); + return this.playerGold -= this.cost; + } if (this.isEnemyDead) { + this.isEnemyDead = false; + return this.playerGold += this.enemyCost; + } + return this.playerGold; + } + + public setGold(gold: number): void { + if (!this.canUpdateGold) { + this.playerGold = gold; + } else { + this.canUpdateGold = false; + } + } + + public getType(): string { + return this.type; + } + + protected hideChoiceTower(): void { + this.towersInfo.forEach((towerButton: Array) => { + towerButton[0].destroy(); + }); + this.towerSelectionCircle.destroy(); + this.closeButton.destroy(); + if (this.infoWindow) { + this.infoWindow.destroy(); + this.textInfo.destroy(); + this.graphics.destroy(); + } + } + + protected createStatsTower(damage: number, speedFire: number, attackArea: number, + physicalDamage: number = 0, magicDamage: number = 0): void { + this.damage = damage; + this.timeForNextShot = speedFire; + this.attackArea = attackArea; + this.physicalDamage = physicalDamage; + this.magicDamage = magicDamage; + } + + public setEnemies(enemies: any): void { + this.enemies = enemies.getChildren(); + } + + public getMissiles(): Phaser.GameObjects.Group { + return this.missiles; + } + + protected getEnemy(): Unit | void { + for (let i = 0; i < this.enemies.length; i += 1) { + this.isEnemyAlive = this.enemies[i].getAlive(); + const enemyDistance = Phaser.Math.Distance.Between(this.x, this.y, this.enemies[i].x, this.enemies[i].y); + if (this.enemies[i].active && this.isEnemyAlive && enemyDistance <= this.attackArea) { + return this.enemies[i]; + } + if (!this.isEnemyAlive && !this.isEnemyDead) { + this.isEnemyDead = true; + this.enemyCost = this.enemies[i].getEnemyCost(); + this.enemies.splice(i, 1); + } + } + } + + protected addMissile(angle: number): void { + const missile = this.missiles.get(); + this.scene.tweens.add({ + targets: this.missiles, + x: this.x, + ease: 'Linear', + duration: 500, + onComplete(tween, targets) { + targets[0].setVisible(false); + }, + }); + if (missile) { + missile.fire(this.x, this.y, angle); + } + } + + public fire(): void { + if (this.isTowerBuilt) { + const enemy = this.getEnemy(); + if (enemy) { + const enemyPositionX = enemy.x; + const enemyPositionY = enemy.y; + const angle = Phaser.Math.Angle.Between(this.x, this.y, enemyPositionX, enemyPositionY); + this.addMissile(angle); + enemy.takeDamage(this.damage, this.physicalDamage, this.magicDamage); + switch (this.type) { + case this.typeArchersTower: + (this.scene as GameScene).sounds.missileArrow.play(); + break; + case this.typeMagicTower: + (this.scene as GameScene).sounds.missileMagic.play(); + break; + case this.typeArtilleryTower: + (this.scene as GameScene).sounds.missileBomb.play(); + break; + } + } + } + } + + update(time: number) { + if (time > this.timeShot && this.isTowerBuilt) { + this.fire(); + this.timeShot = time + this.timeForNextShot; + } + } +} diff --git a/frontend/src/scripts/components/unit/Enemy.ts b/frontend/src/scripts/components/unit/Enemy.ts new file mode 100644 index 0000000..437e3f2 --- /dev/null +++ b/frontend/src/scripts/components/unit/Enemy.ts @@ -0,0 +1,36 @@ +// import 'phaser'; +import Unit from './Unit'; + +export default class Enemy extends Unit { + enemyType: string; + + position: {x: number, y: number}; + + size: number; + + hp: number; + + damage: number; + + physicalArmor: number; + + magicArmor: number; + + killReward: number; + + sprite: string; + + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, x: number, y: number, unitType: string) { + super(scene, way, x, y, unitType); + this.init(); + } + + init() { + this.hp = 100; + this.maxHp = this.hp; + this.damage = 20; + this.physicalArmor = 0; + this.magicArmor = 0; + this.killReward = 5; + } +} diff --git a/frontend/src/scripts/components/unit/EnemyFactory.ts b/frontend/src/scripts/components/unit/EnemyFactory.ts new file mode 100644 index 0000000..a1c23de --- /dev/null +++ b/frontend/src/scripts/components/unit/EnemyFactory.ts @@ -0,0 +1,33 @@ +import Scorpio from './Scorpio'; +import WizardBlack from './WizardBlack'; +import LittleOrc from './LittleOrc'; +import Levendor from './Levendor'; + +export default class EnemyFactory { + context: any; + + x: number; + + y: number; + + props: object; + + constructor(context, x, y) { + this.context = context; + this.x = x; + this.y = y; + } + + static list = { + scorpio: Scorpio, + wizardBlack: WizardBlack, + littleOrc: LittleOrc, + levendor: Levendor, + } + + create(type = 'littleOrc', path) { + const EnemyUnit = EnemyFactory.list[type]; + const enemy = new EnemyUnit(this.context, path, this.x, this.y); + return enemy; + } +} diff --git a/frontend/src/scripts/components/unit/Levendor.ts b/frontend/src/scripts/components/unit/Levendor.ts new file mode 100644 index 0000000..44f10d3 --- /dev/null +++ b/frontend/src/scripts/components/unit/Levendor.ts @@ -0,0 +1,17 @@ +import Enemy from './Enemy'; + +export default class Levendor extends Enemy { + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, x: number, y: number, difficultyCoeff = 1) { + super(scene, way, x, y, 'levendor'); + this.setPosition(x, y); + + // moveSpeed - За какое время будет пройден way + this.moveSpeed = 55000; + this.hp = 150 * difficultyCoeff; + this.maxHp = this.hp; + this.physicalArmor = 15 * difficultyCoeff; + this.magicArmor = 20 * difficultyCoeff; + this.killReward = 25; + this.setScale(0.5); + } +} diff --git a/frontend/src/scripts/components/unit/LittleOrc.ts b/frontend/src/scripts/components/unit/LittleOrc.ts new file mode 100644 index 0000000..c2e9e87 --- /dev/null +++ b/frontend/src/scripts/components/unit/LittleOrc.ts @@ -0,0 +1,16 @@ +import Enemy from './Enemy'; + +export default class LittleOrc extends Enemy { + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, x: number, y: number, difficultyCoeff = 1) { + super(scene, way, x, y, 'littleOrc'); + this.setPosition(x, y); + + // moveSpeed - За какое время будет пройден way + this.moveSpeed = 25000; + this.hp = 25 * difficultyCoeff; + this.maxHp = this.hp; + this.physicalArmor = 5 * difficultyCoeff; + this.killReward = 5; + this.setScale(0.25); + } +} diff --git a/frontend/src/scripts/components/unit/Scorpio.ts b/frontend/src/scripts/components/unit/Scorpio.ts new file mode 100644 index 0000000..625481b --- /dev/null +++ b/frontend/src/scripts/components/unit/Scorpio.ts @@ -0,0 +1,16 @@ +import Enemy from './Enemy'; + +export default class Scorpio extends Enemy { + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, x: number, y: number, difficultyCoeff = 1) { + super(scene, way, x, y, 'scorpio'); + this.setPosition(x, y); + + // moveSpeed - За какое время будет пройден way + this.moveSpeed = 30000; + this.hp = 200 * difficultyCoeff; + this.maxHp = this.hp; + this.physicalArmor = 50 * difficultyCoeff; + this.killReward = 15; + this.setScale(0.75); + } +} diff --git a/frontend/src/scripts/components/unit/Unit.ts b/frontend/src/scripts/components/unit/Unit.ts new file mode 100644 index 0000000..18f0b89 --- /dev/null +++ b/frontend/src/scripts/components/unit/Unit.ts @@ -0,0 +1,103 @@ +// clacc Unit - базовый класс для всех атакующих персонажей: +// как наших защитников, так и врагов + +import 'phaser'; +import { isKiller, isFirstBlood } from '../../constants/achievements'; +import { PlayerStatsManager } from '../stats/PlayerStats'; +import GameScene from '../scenes/GameScene'; + +export default class Unit extends Phaser.GameObjects.PathFollower { + hp: number; + + maxHp: number; + + physicalArmor: number; + + magicArmor: number; + + damage: number; + + damageSpeed: number; + + moveSpeed: number; + + killReward: number; + + isAlive: boolean; + + unitType: string; + + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, + x: number, y: number, unitType: string, difficultyCoeff = 1) { + super(scene, way, x, y, unitType); + scene.add.existing(this); + + this.unitType = unitType; + this.isAlive = true; + this.hp = 100 * difficultyCoeff; + this.physicalArmor = 0 * difficultyCoeff; + this.magicArmor = 0 * difficultyCoeff; + this.damage = 20; + this.damageSpeed = 5; + this.moveSpeed = 10000; + this.killReward = 5; + + this.play({ key: `${this.unitType}_walk`, repeat: Infinity }); + this.setInteractive(); + // this.on("pointerdown", this.takeDamage, this); + } + + takeDamage(damage, physicalDamage, magicDamage) { + if (this.hp <= damage) { + this.hp = 0; + this.die(); + } else if (this.hp > damage) { + if (this.physicalArmor !== 0 && physicalDamage !== 0 && this.physicalArmor >= physicalDamage) { + // this.scene.sound.play(`${this.unitType}-hurt`); + this.physicalArmor -= physicalDamage; + this.play({ key: `${this.unitType}_hurt`, repeat: 0 }); + this.chain([{ key: `${this.unitType}_walk`, repeat: Infinity }]); + } else if (this.magicArmor !== 0 && magicDamage !== 0 && this.magicArmor >= magicDamage) { + this.magicArmor -= magicDamage; + this.play({ key: `${this.unitType}_hurt`, repeat: 0 }); + this.chain([{ key: `${this.unitType}_walk`, repeat: Infinity }]); + } else { + this.hp -= damage; + this.play({ key: `${this.unitType}_hurt`, repeat: 0 }); + this.chain([{ key: `${this.unitType}_walk`, repeat: Infinity }]); + } + } + } + + die() { + if (this.isAlive) { + this.isAlive = false; + this.pauseFollow(); + (this.scene as GameScene).sounds[`${this.unitType}Die`].play(); + this.play({ key: `${this.unitType}_die`, repeat: 0 }); + this.on('animationcomplete', this.despawn, this); + } + } + + despawn() { + const playerStats = new PlayerStatsManager(); + let { killedEnemies } = playerStats.getFromLocalStorage(); + killedEnemies += 1; + isFirstBlood(this.scene); + isKiller(this.scene); + playerStats.saveToLocalStorage({ killedEnemies }); + let deathCounter = this.scene.registry.get('deathCounter'); + deathCounter += 1; + this.scene.registry.set('deathCounter', deathCounter); + + this.scene.time.delayedCall(5000, this.destroy, [], this); + } + + getEnemyCost() { + return this.killReward; + } + + getAlive() { + return this.isAlive; + } +} diff --git a/frontend/src/scripts/components/unit/WizardBlack.ts b/frontend/src/scripts/components/unit/WizardBlack.ts new file mode 100644 index 0000000..073306d --- /dev/null +++ b/frontend/src/scripts/components/unit/WizardBlack.ts @@ -0,0 +1,16 @@ +import Enemy from './Enemy'; + +export default class WizardBlack extends Enemy { + constructor(scene: Phaser.Scene, way: Phaser.Curves.Path, x: number, y: number, difficultyCoeff = 1) { + super(scene, way, x, y, 'wizardBlack'); + this.setPosition(x, y); + + // moveSpeed - За какое время будет пройден way + this.moveSpeed = 50000; + this.hp = 65 * difficultyCoeff; + this.maxHp = this.hp; + this.magicArmor = 20 * difficultyCoeff; + this.killReward = 10; + this.setScale(0.3); + } +} diff --git a/frontend/src/scripts/components/unit/createAnims.ts b/frontend/src/scripts/components/unit/createAnims.ts new file mode 100644 index 0000000..5abd301 --- /dev/null +++ b/frontend/src/scripts/components/unit/createAnims.ts @@ -0,0 +1,109 @@ +export default function createAnims(context) { + context.anims.create({ + key: 'scorpio_walk', + frames: context.anims.generateFrameNumbers('scorpio', { + start: 0, + end: 19, + }), + frameRate: 80, + }); + + context.anims.create({ + key: 'scorpio_die', + frames: context.anims.generateFrameNumbers('scorpio_die', { + start: 0, + end: 19, + }), + frameRate: 60, + }); + + context.anims.create({ + key: 'scorpio_hurt', + frames: context.anims.generateFrameNumbers('scorpio_hurt', { + start: 0, + end: 19, + }), + frameRate: 80, + }); + + context.anims.create({ + key: 'wizardBlack_walk', + frames: context.anims.generateFrameNumbers('wizardBlack', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'wizardBlack_die', + frames: context.anims.generateFrameNumbers('wizardBlack_die', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'wizardBlack_hurt', + frames: context.anims.generateFrameNumbers('wizardBlack_hurt', { + start: 0, + end: 19, + }), + frameRate: 30, + }); + + context.anims.create({ + key: 'littleOrc_walk', + frames: context.anims.generateFrameNumbers('littleOrc', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'littleOrc_die', + frames: context.anims.generateFrameNumbers('littleOrc_die', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'littleOrc_hurt', + frames: context.anims.generateFrameNumbers('littleOrc_hurt', { + start: 0, + end: 19, + }), + frameRate: 30, + }); + + context.anims.create({ + key: 'levendor_walk', + frames: context.anims.generateFrameNumbers('levendor', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'levendor_die', + frames: context.anims.generateFrameNumbers('levendor_die', { + start: 0, + end: 19, + }), + frameRate: 25, + }); + + context.anims.create({ + key: 'levendor_hurt', + frames: context.anims.generateFrameNumbers('levendor_hurt', { + start: 0, + end: 19, + }), + frameRate: 50, + }); +} diff --git a/frontend/src/scripts/constants/achievements.ts b/frontend/src/scripts/constants/achievements.ts new file mode 100644 index 0000000..716b1fa --- /dev/null +++ b/frontend/src/scripts/constants/achievements.ts @@ -0,0 +1,128 @@ +import sendDataToBackend from '../achievements/utils/backend'; +import { PlayerStatsManager } from '../components/stats/PlayerStats'; +import Popup from '../components/events/achievements_popup'; + +const playerStats = new PlayerStatsManager(); + +function isGreatDefender(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { gameProgress } = statsData; + const values: Array = Object.values(gameProgress); + + if (statsData.achievements.greatDefender !== true) { + if (values.reduce((a: number, b: number) => a + b, 0) === 9) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('greatDefender'); + popup.startAnimation(); + + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, greatDefender: true } }); + sendDataToBackend(); + } + } +} + +function isIronDefender(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { ironModeProgress } = statsData; + const values: Array = Object.values(ironModeProgress); + + if (statsData.achievements.ironDefender !== true) { + if (values.reduce((a: number, b: number) => a + b, 0) === 9) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('ironDefender'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, ironDefender: true } }); + sendDataToBackend(); + } + } +} + +function isCompleteWin(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { gameProgress } = statsData; + const values: Array = Object.values(gameProgress); + + if (statsData.achievements.completeWin !== true) { + if (values.length === 3 && values.indexOf(0) === -1) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('completeWin'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, completeWin: true } }); + sendDataToBackend(); + } + } +} + +function isFirstAsterisk(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { gameProgress } = statsData; + const { ironModeProgress } = statsData; + const gameValues: Array = Object.values(gameProgress); + const ironModeValues: Array = Object.values(ironModeProgress); + + if (statsData.achievements.firstAsterisk !== true) { + if ((gameValues.indexOf(1) !== -1 || gameValues.indexOf(2) !== -1 || gameValues.indexOf(3) !== -1) + || (ironModeValues.indexOf(1) !== -1 || ironModeValues.indexOf(2) !== -1 || ironModeValues.indexOf(3) !== -1)) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('firstAsterisk'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, firstAsterisk: true } }); + sendDataToBackend(); + } + } +} + +function isBuilder(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { builtTowers } = statsData; + if (builtTowers === 30) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('builder'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, builder: true } }); + sendDataToBackend(); + } +} + +function isFirstBlood(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { killedEnemies } = statsData; + + if (killedEnemies === 1) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('firstBlood'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, firstBlood: true } }); + sendDataToBackend(); + } +} + +function isKiller(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { killedEnemies } = statsData; + + if (killedEnemies === 150) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('killer'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, killer: true } }); + sendDataToBackend(); + } +} + +function isSeller(scene) { + const statsData = playerStats.getFromLocalStorage(); + const { soldTowers } = statsData; + + if (soldTowers === 30) { + const popup = new Popup(scene, 0, 0, 'achievementPopup'); + popup.init('seller'); + popup.startAnimation(); + playerStats.saveToLocalStorage({ achievements: { ...statsData.achievements, seller: true } }); + sendDataToBackend(); + } +} + +export { + isBuilder, isSeller, isKiller, isIronDefender, isGreatDefender, isFirstBlood, isFirstAsterisk, isCompleteWin, +}; diff --git a/frontend/src/scripts/constants/constants.ts b/frontend/src/scripts/constants/constants.ts new file mode 100644 index 0000000..479321d --- /dev/null +++ b/frontend/src/scripts/constants/constants.ts @@ -0,0 +1,194 @@ +import { map1, map2, map3 } from './maps'; + +const RANDOM_WAY_COEFFICIENT = 15; +const KEY_TOKEN = 'token-KR-clone'; +const KEY_ID = 'id-KR-clone'; +const LOCAL_STORAGE_KEY = 'KingdomRushCloneStateKey'; + +const levelsConfig = { + level_1: { + map: map1, + startingGold: 250, + waves: { + wave_1: { + enemies: { + littleOrc: 3, + }, + }, + wave_2: { + enemies: { + littleOrc: 6, + }, + }, + wave_3: { + enemies: { + littleOrc: 10, + }, + }, + wave_4: { + enemies: { + littleOrc: 5, + }, + }, + wave_5: { + enemies: { + littleOrc: 15, + }, + }, + wave_6: { + enemies: { + littleOrc: 20, + }, + }, + }, + theme: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pharetra urna quis tristique posuere. Curabitur iaculis nulla porta dui maximus porta. Mauris vehicula facilisis sapien in hendrerit. Etiam a leo ac eros accumsan placerat. Praesent vestibulum rutrum magna non tristique. Maecenas sem massa, pretium ac ante nec, volutpat luctus nibh. Maecenas vulputate fringilla porta. Sed et justo non erat laoreet semper.', + }, + level_2: { + map: map2, + startingGold: 350, + waves: { + wave_1: { + enemies: { + littleOrc: 10, + }, + }, + wave_2: { + enemies: { + wizardBlack: 4, + littleOrc: 10, + }, + }, + wave_3: { + enemies: { + wizardBlack: 10, + }, + }, + wave_4: { + enemies: { + littleOrc: 5, + wizardBlack: 4, + levendor: 1, + }, + }, + wave_5: { + enemies: { + littleOrc: 15, + wizardBlack: 10, + }, + }, + wave_6: { + enemies: { + scorpio: 1, + littleOrc: 20, + }, + }, + wave_7: { + enemies: { + scorpio: 2, + littleOrc: 10, + wizardBlack: 4, + }, + }, + }, + theme: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pharetra urna quis tristique posuere. Curabitur iaculis nulla porta dui maximus porta. Mauris vehicula facilisis sapien in hendrerit. Etiam a leo ac eros accumsan placerat. Praesent vestibulum rutrum magna non tristique. Maecenas sem massa, pretium ac ante nec, volutpat luctus nibh. Maecenas vulputate fringilla porta. Sed et justo non erat laoreet semper.', + }, + level_3: { + map: map3, + startingGold: 400, + waves: { + wave_1: { + enemies: { + littleOrc: 10, + }, + }, + wave_2: { + enemies: { + littleOrc: 10, + wizardBlack: 10, + }, + }, + wave_3: { + enemies: { + scorpio: 1, + wizardBlack: 4, + }, + }, + wave_4: { + enemies: { + littleOrc: 10, + wizardBlack: 5, + }, + }, + wave_5: { + enemies: { + scorpio: 2, + wizardBlack: 4, + }, + }, + wave_6: { + enemies: { + littleOrc: 25, + }, + }, + wave_7: { + enemies: { + littleOrc: 20, + wizardBlack: 10, + }, + }, + wave_8: { + enemies: { + littleOrc: 10, + }, + }, + wave_9: { + enemies: { + littleOrc: 10, + wizardBlack: 6, + }, + }, + wave_10: { + enemies: { + scorpio: 1, + littleOrc: 15, + wizardBlack: 10, + }, + }, + wave_11: { + enemies: { + scorpio: 5, + wizardBlack: 10, + }, + }, + wave_12: { + enemies: { + littleOrc: 10, + }, + }, + wave_13: { + enemies: { + scorpio: 2, + littleOrc: 15, + wizardBlack: 4, + }, + }, + wave_14: { + enemies: { + scorpio: 8, + wizardBlack: 10, + }, + }, + wave_15: { + enemies: { + scorpio: 2, + littleOrc: 20, + }, + }, + }, + theme: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pharetra urna quis tristique posuere. Curabitur iaculis nulla porta dui maximus porta. Mauris vehicula facilisis sapien in hendrerit. Etiam a leo ac eros accumsan placerat. Praesent vestibulum rutrum magna non tristique. Maecenas sem massa, pretium ac ante nec, volutpat luctus nibh. Maecenas vulputate fringilla porta. Sed et justo non erat laoreet semper.', + }, +}; + +export { + RANDOM_WAY_COEFFICIENT, LOCAL_STORAGE_KEY, levelsConfig, KEY_TOKEN, KEY_ID, +}; diff --git a/frontend/src/scripts/constants/maps.ts b/frontend/src/scripts/constants/maps.ts new file mode 100644 index 0000000..fe99430 --- /dev/null +++ b/frontend/src/scripts/constants/maps.ts @@ -0,0 +1,144 @@ +export type MapType = { + level: number; + url: string, + scalePointsWay: Array, + scaleStartPointX: number, + scaleStartPointY: number, + scaleFinishPointX: number, + scaleFinishPointY: number, + scaleCoordinateTowers: Array, +}; + +const map1: MapType = { + level: 1, + url: '../assets/imgs/forest_scene2.jpg', + scalePointsWay: [ + { x: 6.4, y: 5.1 }, + { x: 3.2, y: 5.7 }, + { x: 2.35, y: 4 }, + { x: 2.25, y: 2.7 }, + { x: 2.95, y: 2.1 }, + { x: 4.2, y: 1.7 }, + { x: 4, y: 1.4 }, + { x: 2.91, y: 1.32 }, + { x: 2, y: 1.47 }, + { x: 1.68, y: 1.79 }, + { x: 1.39, y: 2.55 }, + { x: 1.25, y: 2.69 }, + { x: 1.11, y: 2.15 }, + { x: 1, y: 2 }, + { x: 0.92, y: 1.84 }, + ], + scaleStartPointX: -100, + scaleStartPointY: 3.1, + scaleFinishPointX: 1, + scaleFinishPointY: 2, + scaleCoordinateTowers: [ + { x: 3.6, y: 3.2 }, + { x: 8, y: 1.74 }, + { x: 2.91, y: 1.6 }, + { x: 3.41, y: 1.13 }, + { x: 1.97, y: 2 }, + { x: 1.95, y: 3.72 }, + { x: 1.73, y: 1.28 }, + { x: 1.42, y: 1.65 }, + { x: 1.19, y: 1.79 }, + { x: 1.06, y: 2.76 }, + ], +}; + +const map2: MapType = { + level: 2, + url: '../assets/imgs/forest_scene1.jpg', + scalePointsWay: [ + [{ x: -102.4, y: 4.48 }, { x: -102.4, y: 2.12 }], + [{ x: 5.12, y: 4.84 }, { x: 10.24, y: 2.12 }], + [{ x: 2.92, y: 6.05 }, { x: 5.12, y: 2.33 }], + [{ x: 2.04, y: 5.26 }, { x: 3.41, y: 2.57 }], + [{ x: 1.74, y: 4.03 }, { x: 2.56, y: 2.69 }], + [{ x: 1.64, y: 2.57 }, { x: 2.09, y: 2.37 }], + { x: 2.15, y: 1.83 }, + { x: 2.73, y: 1.59 }, + { x: 2.97, y: 1.4 }, + { x: 2.41, y: 1.27 }, + { x: 1.95, y: 1.36 }, + { x: 1.64, y: 1.47 }, + { x: 1.41, y: 1.61 }, + { x: 1.24, y: 1.61 }, + { x: 1.1, y: 1.59 }, + { x: 1, y: 1.55 }, + { x: 0.95, y: 1.59 }, + ], + scaleStartPointX: -250, + scaleStartPointY: 3.2, + scaleFinishPointX: 1, + scaleFinishPointY: 1.57, + scaleCoordinateTowers: [ + { x: 1.78, y: 1.19 }, + { x: 1.55, y: 1.26 }, + { x: 3.53, y: 1.2 }, + { x: 2.38, y: 1.46 }, + { x: 1.99, y: 1.57 }, + { x: 1.7, y: 1.75 }, + { x: 2.56, y: 2.05 }, + { x: 1.91, y: 2.95 }, + { x: 2.27, y: 3.36 }, + { x: 2.8, y: 3.46 }, + { x: 1.7, y: 6.37 }, + ], +}; + +const map3: MapType = { + level: 3, + url: '../assets/imgs/desert_scene3.jpg', + scalePointsWay: [ + { x: 10.24, y: 1.75 }, + [{ x: 7.87, y: 1.86 }, { x: 5.69, y: 1.51 }], + [{ x: 6.83, y: 2.2 }, { x: 4.87, y: 1.42 }], + [{ x: 4.87, y: 2.69 }, { x: 4.1, y: 1.37 }], + [{ x: 3.94, y: 3.61 }, { x: 3.47, y: 1.36 }], + [{ x: 3.25, y: 4.24 }, { x: 3.1, y: 1.37 }], + [{ x: 2.84, y: 4.17 }, { x: 2.73, y: 1.37 }], + [{ x: 2.27, y: 3.9 }, { x: 2.27, y: 1.39 }], + [{ x: 1.86, y: 3.27 }, { x: 1.86, y: 1.46 }], + [{ x: 1.74, y: 3.1 }, { x: 1.64, y: 1.44 }], + [{ x: 1.64, y: 3.27 }, { x: 1.52, y: 1.4 }], + [{ x: 1.46, y: 3.27 }, { x: 1.36, y: 1.36 }], + [{ x: 1.36, y: 2.88 }, { x: 1.28, y: 1.39 }], + [{ x: 1.28, y: 2.12 }, { x: 1.2, y: 1.51 }], + [{ x: 1.22, y: 2.05 }, { x: 1.19, y: 1.86 }], + { x: 1.18, y: 2.2 }, + { x: 1.16, y: 3.1 }, + { x: 1.13, y: 4.03 }, + { x: 1.08, y: 6.05 }, + { x: 1.04, y: 8.06 }, + { x: 1, y: 9.3 }, + { x: 0.95, y: 9.3 }, + ], + scaleStartPointX: -100, + scaleStartPointY: 1.75, + scaleFinishPointX: 1, + scaleFinishPointY: 7.4, + scaleCoordinateTowers: [ + { x: 12.8, y: 2.52 }, + { x: 12.8, y: 1.4 }, + { x: 4.45, y: 1.7 }, + { x: 3.25, y: 1.18 }, + { x: 3.05, y: 2.81 }, + { x: 3.01, y: 6.37 }, + { x: 1.86, y: 4.65 }, + { x: 1.79, y: 1.25 }, + { x: 2.35, y: 1.57 }, + { x: 1.38, y: 1.55 }, + { x: 1.55, y: 2.42 }, + { x: 1.28, y: 3.78 }, + { x: 1.05, y: 2.12 }, + { x: 1.1, y: 1.32 }, + ], +}; + +export { + map1, + map2, + map3, +}; diff --git a/frontend/src/scripts/constants/waveBtnConfigs.ts b/frontend/src/scripts/constants/waveBtnConfigs.ts new file mode 100644 index 0000000..37897a0 --- /dev/null +++ b/frontend/src/scripts/constants/waveBtnConfigs.ts @@ -0,0 +1,25 @@ +const waveBtnConfigs = { + 1: { + startPointX: 200, + startPointY: -50, + endPointX: 50, + endPointY: -20, + rotation: 0.4, + }, + 2: { + startPointX: 340, + startPointY: 210, + endPointX: 50, + endPointY: 0, + rotation: 0, + }, + 3: { + startPointX: 200, + startPointY: 30, + endPointX: 50, + endPointY: 0, + rotation: 0, + }, +}; + +export default waveBtnConfigs; diff --git a/frontend/src/scripts/credits/create.credits.ts b/frontend/src/scripts/credits/create.credits.ts new file mode 100644 index 0000000..583aea4 --- /dev/null +++ b/frontend/src/scripts/credits/create.credits.ts @@ -0,0 +1,105 @@ +import createElement from '../auth/utils/createElement'; +import { whileLoad, whileRaise } from '../auth/utils/wait.while.loading'; +import langConfig from '../layouts/langConfig'; + +function createCredits() { + const lang = window['lang'] || localStorage.getItem('lang') || 'en'; + const creditsText = langConfig[`${lang}`].credits; + const ourTeamText = langConfig[`${lang}`].ourTeam; + const gratitudeText = langConfig[`${lang}`].gratitude; + const specialThanksFromText = langConfig[`${lang}`].specialThanksFrom; + const myTeamText = langConfig[`${lang}`].myTeam; + const perText = langConfig[`${lang}`].per; + const andEnduranceText = langConfig[`${lang}`].andEndurance; + const andPeopleFromChatsText = langConfig[`${lang}`].andPeopleFromChats; + const myTurtleText = langConfig[`${lang}`].myTurtle; + const forStressReliefAndBuoyancyText = langConfig[`${lang}`].forStressReliefAndBuoyancy; + const forMentoringAndStrongShoulderText = langConfig[`${lang}`].forMentoringAndStrongShoulder; + const forJokesCommunicationAndHelpText = langConfig[`${lang}`].forJokesCommunicationAndHelp; + const forEverythingAndEvenMoreText = langConfig[`${lang}`].forEverythingAndEvenMore; + + const popup = createElement('div', { + classList: ['credits-wrapper'], + innerHTML: ` +
+ +
+ +
+

${creditsText[0].toUpperCase() + creditsText.slice(1)}

+

${ourTeamText}

+ +

${gratitudeText}

+
    +
    + ${specialThanksFromText} IogSotot: +
    +
  • ${myTeamText}
  • +
  • ${perText} Kingdom Rush ${andEnduranceText}
  • +
  • ${myTurtleText}
  • +
  • ${forStressReliefAndBuoyancyText}
  • +
  • Артёму Cardamo
  • +
  • ${forMentoringAndStrongShoulderText}
  • +
  • RSS ${andPeopleFromChatsText}
  • +
  • ${forJokesCommunicationAndHelpText}
  • +
  • Алексею Lexlem
  • +
  • ${forEverythingAndEvenMoreText}
  • +
+
    +
    + ${specialThanksFromText} DenisAfa: +
    +
  • ${myTeamText}
  • +
  • ${perText} Kingdom Rush ${andEnduranceText}
  • +
+
    +
    + ${specialThanksFromText} Abdulloh76: +
    +
  • ${myTeamText}
  • +
  • ${perText} Kingdom Rush ${andEnduranceText}
  • +
+ + + +
+
+
+ +
+ `, + onclick: ({ target }) => { + if (target.classList.contains('close-credits-popup')) { + whileRaise(popup); + } + }, + }); + + whileLoad(popup, '../assets/credits/wood_2.png'); +} + +export default createCredits; diff --git a/frontend/src/scripts/credits/scss/credits.scss b/frontend/src/scripts/credits/scss/credits.scss new file mode 100644 index 0000000..1db79f3 --- /dev/null +++ b/frontend/src/scripts/credits/scss/credits.scss @@ -0,0 +1,256 @@ +.credits-wrapper { + position: fixed; + z-index: 35; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: url("../assets/credits/wood_2.png"); + background-repeat: no-repeat; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; + cursor: default; + + .close-credits-popup { + background-image: url("../assets/interface/button_close.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 50px; + height: 50px; + position: fixed; + margin: 15px; + left: 0; + top: 0; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } + } + + .credits-bg { + height: 100vh; + width: 100vw; + + padding: 12vh 0 15vh; + + background-image: url("../assets/credits/paper_3.png"); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + + bottom: 100%; + transition: all .5s ease-out; + position: relative; + } + + .show-top { + bottom: 0; + } + + .credits-data { + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + overflow-y: scroll; + + &::-webkit-scrollbar { + width: 5px; + } + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 1px rgb(212, 232, 37); + } + &::-webkit-scrollbar-thumb { + background: #c43c1c; + border-radius: 0.25vw; + } + + height: 73vh; + width: fit-content; + margin: auto; + padding: 5px 10px 5px 5px; + + font-family: "Dimbo", sans-serif; + font-size: 16px; + color: #c43c1c; + text-align: center; + + .credits-title { + height: 65px; + padding: 5px; + margin: 5px auto 0px; + + background-image: url("../assets/auth/title.png"); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + + font-size: 26px; + color: #5f3616; + } + + .credits-subtitle { + margin-top: 15px; + + font-size: 24px; + letter-spacing: 1px; + color: #884d20; + } + + .team { + display: flex; + width: 400px; + flex-wrap: wrap; + + margin: auto; + } + + .team-people { + display: flex; + flex-direction: row; + align-items: center; + + width: 190px; + margin: 3px; + padding: 2px; + + border: 2px solid #d0a763; + background-color: #e3b872; + + font-size: 20px; + text-decoration: none; + + transition: all 250ms linear; + &:nth-of-type(1) { + border-top-left-radius: 20px; + } + &:nth-of-type(2) { + border-top-right-radius: 20px; + } + &:nth-of-type(3) { + border-bottom-left-radius: 20px; + } + &:nth-of-type(4) { + border-bottom-right-radius: 20px; + } + + .avatar { + height: 80px; + width: 80px; + margin-right: 15px; + + border-radius: 50%; + background-size: contain; + &.Iogsotot { + background-image: url("https://avatars.githubusercontent.com/u/50149163?s=400&u=9a763fefd25839d62bec939ad7302d8a29948cb4&v=4"); + } + &.DenisAfa { + background-image: url("https://avatars.githubusercontent.com/u/64201928?s=400&u=b33f2c5f182a021649e8ac548259a05dc3ed792e&v=4"); + } + &.Abdulloh76 { + background-image: url("https://avatars.githubusercontent.com/u/59783836?s=400&v=4"); + } + &.mrINEX { + background-image: url("https://avatars.githubusercontent.com/u/35580404?s=400&u=742a3a17f24933ff191b620cf949e6f1b872e20a&v=4"); + } + } + + .team-member { + color: antiquewhite; + letter-spacing: 1px; + margin: auto; + transition: all 350ms linear; + } + + &:hover { + transition: all 350ms linear; + background-color: #f5d4a0; + .team-member { + transition: all 350ms linear; + color: #a1814c; + } + } + } + + ul { + list-style: none; + + .gratitude-title { + font-size: 18px; + font-style: italic; + padding: 10px 0 5px; + } + & li { + font-size: 18px; + } + & li:nth-child(odd) { + font-size: 14px; + font-weight: normal; + padding: 0px 0 10px; + } + } + .credits-link { + color: #e68303; + text-decoration: none; + font-weight: 500; + } + + .wrapper-gratitude { + display: flex; + flex-direction: column; + } + } +} + +@media (min-height: 950px) { + .credits-wrapper { + .credits-data { + .team-people { + font-size: 22px; + } + .credits-subtitle { + font-size: 26px; + } + + ul { + li { + font-size: 20px; + &:nth-child(odd) { + font-size: 16px; + } + } + .gratitude-title { + font-size: 20px; + } + } + } + } +} + +@media (min-height: 1100px) { + .credits-wrapper { + .credits-data { + .credits-subtitle { + font-size: 28px; + } + + ul { + li { + font-size: 22px; + &:nth-child(odd) { + font-size: 18px; + } + } + .gratitude-title { + font-size: 22px; + } + } + } + } +} diff --git a/frontend/src/scripts/layouts/en.ts b/frontend/src/scripts/layouts/en.ts new file mode 100644 index 0000000..9b7c3d8 --- /dev/null +++ b/frontend/src/scripts/layouts/en.ts @@ -0,0 +1,92 @@ +export default { + signIn: 'sign in', + signUp: 'sign up', + name: 'name', + password: 'password', + hasSignIn: 'has sign in', + hasSignUp: 'has sign up', + keepMe: 'Keep Me Signed In', + playerName: 'Player\'s name', + gameProgress: 'game progress', + start: 'start', + credits: 'credits', + attendanceOverYear: 'Game attendance over the year', + statistics: 'Statistics', + profile: 'Profile', + overallRating: 'Overall rating', + achievements: 'achievements', + youGot: 'You got', + achievementsOutOf: 'achievements out of', + all: 'all', + ourTeam: 'Our team', + gratitude: 'Gratitude', + specialThanksFrom: 'Special thanks from', + myTeam: 'my team', + per: 'per', + andEndurance: 'and endurance!', + andPeopleFromChats: 'and people from chats', + myTurtle: 'my turtle', + forStressReliefAndBuoyancy: 'for stress relief and buoyancy', + forMentoringAndStrongShoulder: 'for a mentoring and strong shoulder', + forJokesCommunicationAndHelp: 'for jokes, communication and help', + forEverythingAndEvenMore: 'for everything and even more', + achievs: { + firstBlood: 'First Blood', + firstAsterisk: 'First Asterisk', + seller: 'Seller', + killer: 'Killer', + builder: 'Builder', + ironDefender: 'Iron Defender', + greatDefender: 'Great Defender', + completeWin: 'Complete Win', + }, + loading: 'Loading', + level: 'level', + easy: 'easy', + normal: 'normal', + hard: 'hard', + towersNumber: 'Possible towers number', + wave: 'wave', + options: 'options', + music: 'Music', + sound: 'Sound', + failed: 'failed', + sorry: 'SORRY :(/LEVEL FAILED', + win: 'you win!', + congrats: 'CONGRATULATIONS!/LEVEL COMPLETE', + slow: 'slow', + medium: 'medium', + fast: 'fast', + small: 'small', + long: 'long', + levelTheme: { + 1: 'Greeting, noble and heroic warrior! As a General of His Mejesty\'s forces, King Denas requires your services. A band of Orcs approaches near the capital! Theay are beasts who only think about looting and burning everything in their wake. It is necessary to stop these crimes before they grow from troubles to problems. ', + 2: 'General, the orc gang was not our biggest problem! The inhabitants of the city of Southport have reported attacks by Black Wizards from the Forbidden Lands. You must take a batalion to Southport to bolster the city\' defenses. Good luck, General!', + 3: 'Defender of Linerea! We are at the gates of ancient Hammerhold. Hordes of Black Mages, Orcs and Scorpions are overrunning the few troops still defenfing the fortress wall. Time is of the essence general, we must rush to their aid!', + }, + enemy: { + littleOrc: 'little orc', + wizardBlack: 'wizard black', + scorpio: 'scorpio', + levendor: 'Levendor', + }, + tower: { + archers: 'ARCHERS TOWER', + artillery: 'ARTILLERY TOWER', + magic: 'MAGIC TOWER', + }, + hotkeysTitle: 'Hot Keys', + hotkeys: [ + { key: 'CTRL+Q', info: 'quit game from levels page' }, + { key: 'KEY NUMBERS', info: 'choose appropriate level' }, + { key: 'KEY D', info: 'change the game difficulty' }, + { key: 'ENTER', info: 'start game(when choosing level)' }, + { key: 'CTRL+M', info: 'go to levels page(when game is paused)' }, + { key: 'ESC', info: 'close modal' }, + { key: 'KEY N', info: 'start first wave' }, + { key: 'CTRL+SPACE', info: 'pause the game' }, + { key: 'CTRL+LEFT/RIGHT', info: 'appropriate button when win/lose' }, + ], + price: 'price', + damage: 'damage', +}; diff --git a/frontend/src/scripts/layouts/langConfig.ts b/frontend/src/scripts/layouts/langConfig.ts new file mode 100644 index 0000000..0349144 --- /dev/null +++ b/frontend/src/scripts/layouts/langConfig.ts @@ -0,0 +1,5 @@ +import en from './en'; +import ru from './ru'; +import uz from './uz'; + +export default { en, ru, uz }; diff --git a/frontend/src/scripts/layouts/ru.ts b/frontend/src/scripts/layouts/ru.ts new file mode 100644 index 0000000..b8c5489 --- /dev/null +++ b/frontend/src/scripts/layouts/ru.ts @@ -0,0 +1,92 @@ +export default { + signIn: 'вход', + signUp: 'регистрация', + name: 'имя', + password: 'пароль', + hasSignIn: 'вошел', + hasSignUp: 'зарегистрировался', + keepMe: 'Держать меня в системе', + playerName: 'Имя игрока', + gameProgress: 'прогресс игры', + start: 'начать', + credits: 'разработчики', + attendanceOverYear: 'Посещаемость игры за год', + statistics: 'Статистика', + profile: 'Профиль', + overallRating: 'Общий рейтинг', + achievements: 'достижения', + youGot: 'Вы получили', + achievementsOutOf: 'достижения из', + all: 'все', + ourTeam: 'Наша команда', + gratitude: 'Благодарность', + specialThanksFrom: 'Особая благодарность от', + myTeam: 'моей команде', + per: 'за', + andEndurance: 'и выдержку!', + andPeopleFromChats: 'и людям из чатов', + myTurtle: 'моей черепахе', + forStressReliefAndBuoyancy: 'за снятие стресса и плавучесть', + forMentoringAndStrongShoulder: 'за менторство и крепкое плечо', + forJokesCommunicationAndHelp: 'за шутки, общение и помощь', + forEverythingAndEvenMore: 'за всё и даже больше', + achievs: { + firstBlood: 'Первая Кровь', + firstAsterisk: 'Первая Звезда', + seller: 'Продавец', + killer: 'Убийца', + builder: 'Строитель', + ironDefender: 'Железный Защитник', + greatDefender: 'Великий Защитник', + completeWin: 'Полная Победа', + }, + loading: 'Загрузка', + level: 'уровень', + easy: 'легкий', + normal: 'обычный', + hard: 'тяжелый', + towersNumber: 'Возможное количество башен', + wave: 'волна', + options: 'Опции', + music: 'Музыка', + sound: 'Звук', + failed: 'проигрыш', + sorry: 'СОРЯН :(/УРОВЕНЬ ПРОВАЛЕН', + win: 'победа!', + congrats: 'ПОЗДРАВЛЯЕМ!/УРОВЕНЬ ПРОЙДЕН', + slow: 'медленный', + medium: 'средний', + fast: 'быстрый', + small: 'маленький', + long: 'большой', + levelTheme: { + 1: 'Приветствую вас и преклоняюсь перед вашим благородством и отвагой! Его величество король Денас вызывает своего генерала на службу. Недалеко от столицы заметили банду орков! Эти твари только и умеют, что грабить да сжигать всё на своём пути. Необходимо пресечь эти преступления, пока они переросли из неприятностей в проблемы.', + 2: 'Генерал, банда орков оказалась не самой большой нашей проблемой! Жители Южного Порта сообщают о нападениях со стороны Чёрных Магов из Запретных Земель. Вас с батальоном отправляют туда, чтобы обеспечить безопасность города. Удачи, генерал!', + 3: 'Защитник Линерии, мы возле ворот древнего Хаммерхолда. Орды Чёрных Магов, Орков и Скорпионов скоро уничтожат последних защитников крепостных стен. Пора вмешаться и протянуть друзьям руку помощи!', + }, + enemy: { + littleOrc: 'маленький орк', + wizardBlack: 'черный волшебник', + scorpio: 'скорпион', + levendor: 'Левендор', + }, + tower: { + archers: 'БАШНЯ ЛУЧНИКОВ', + artillery: 'БАШНЯ АРТИЛЛЕРИЙ', + magic: 'БАШНЯ МАГА', + }, + hotkeysTitle: 'Кнопки', + hotkeys: [ + { key: 'CTRL+Q', info: 'выйти из игры(с карты уровней)' }, + { key: 'KEY NUMBERS', info: 'выбрать подходящий уровень' }, + { key: 'KEY D', info: 'изменить сложность игры' }, + { key: 'ENTER', info: 'начать игру(при выборе уровня)' }, + { key: 'CTRL+M', info: 'перейти на страницу уровней(когда игра приостановлена)' }, + { key: 'ESC', info: 'закрыть модальное окно' }, + { key: 'KEY N', info: 'начать битву!' }, + { key: 'CTRL+SPACE', info: 'приостановить игру' }, + { key: 'CTRL+LEFT/RIGHT', info: 'соответсвующая кнопка при выигрыше/проигрыше' }, + ], + price: 'стоимость', + damage: 'урон', +}; diff --git a/frontend/src/scripts/layouts/uz.ts b/frontend/src/scripts/layouts/uz.ts new file mode 100644 index 0000000..5fb3737 --- /dev/null +++ b/frontend/src/scripts/layouts/uz.ts @@ -0,0 +1,92 @@ +export default { + signIn: 'kirish', + signUp: 'ro\'yxatdan o\'tish', + name: 'ism', + password: 'parol', + hasSignIn: 'tizimga kirdi', + hasSignUp: 'ro\'yxatdan o\'tdi', + keepMe: 'Meni eslab qolish', + playerName: 'O\'yinchining ismi', + gameProgress: 'o\'yin rivojlanishi', + start: 'boshlash', + credits: 'mualliflar', + attendanceOverYear: 'Yillik tashriflar soni', + statistics: 'Statistika', + profile: 'Profil', + overallRating: 'Umumiy reyting', + achievements: 'yutuqlar', + youGot: 'Ta yutuqdan', + achievementsOutOf: 'tasiga erishdingiz', + all: 'barchasi', + ourTeam: 'Bizning jamoamiz', + gratitude: 'Minnatdorchilik', + specialThanksFrom: 'Maxsus minnatdorchilik', + myTeam: 'mening jamoam', + per: 'per', + andEndurance: 'va chidamlilik!', + myTurtle: 'mening toshbaqam', + forStressReliefAndBuoyancy: 'stressni yo\'qotish va ko\'tarish uchun', + forMentoringAndStrongShoulder: 'murabbiylik va kuchli elkasi uchun', + forJokesCommunicationAndHelp: 'hazillar, aloqa va yordam uchun', + forEverythingAndEvenMore: 'hamma narsa uchun va hatto undan ham ko\'proq', + achievs: { + firstBlood: 'Birinchi Qon', + firstAsterisk: 'Birinchi Yulduz', + andPeopleFromChats: 'va suhbatdoshlar', + seller: 'Sotuvchi', + killer: 'Qotil', + builder: 'Quruvchi', + ironDefender: 'Kuchli Himoyachi', + greatDefender: 'Ajoyib Himoyachi', + completeWin: 'To\'liq G\'alaba', + }, + loading: 'Yuklanmoqda', + level: 'bosqich', + easy: 'oson', + normal: 'oddiy', + hard: 'qiyin', + towersNumber: 'Minoralar uchun joylar soni', + wave: 'to\'lqin', + options: 'sozlamalar', + music: 'Musiqa', + sound: 'Tovush', + failed: 'mag\'lubiyat', + sorry: 'UZUR :(/MUVAFFAQIYATSIZ/YAKUN', + win: 'g\'alaba!', + congrats: 'TABRIKLAR!/MUVAFFAQIYATLI/YAKUN', + slow: 'sekin', + medium: 'o\'rtacha', + fast: 'tez', + small: 'kichik', + long: 'katta', + levelTheme: { + 1: 'Sizga salom beraman va sizning olijanobligingiz va jasoratingiz oldida bosh egaman! Ulug\'vor podshoh Denas o\'z generalini xizmatga chaqiradi. Poytaxt yaqinida orklar to\'dasi ko\'rindi! Bu jonzotlar faqat o\'z yo\'llaridagi hamma narsani talon-taroj qilishni va yoqishni biladilar. Ushbu jinoyatlar o\'sishdan oldin ularni to\'xtatish kerak.', + 2: 'General, orklar to\'dasi bizning eng katta muammoimiz emas bo\'lib chiqdi! Janubiy port aholisi Taqiqlangan Yerlardan Qora Sehrgarlar tomonidan qilingan hujumlar haqida xabar berishdi. Siz va sizning batalyoningiz shahar xavfsizligini ta\'minlash uchun u yerga safarbar qilindingiz. Omad tilaymiz, general!', + 3: 'Lineriya himoyachisi, biz qadimgi Hammerhold darvozasi oldidamiz. Qora Sehrgarlar, Orklar va Chayonlar qo\'shinlari qal\'a devorlarining so\'nggi himoyachilarini tez orada yo\'q qiladilar. Bunga aralashish va do\'stlaringizga yordam qo\'lini cho\'zish vaqti keldi!', + }, + enemy: { + littleOrc: 'kichik ork', + wizardBlack: 'qora sehrgar', + scorpio: 'chayon', + levendor: 'Levendor', + }, + tower: { + archers: 'KAMONCHILAR MINORASI', + artillery: 'ARTILLERYA MINORASI', + magic: 'SEHRGAR MINORASI', + }, + hotkeysTitle: 'Tezkor Tugmalar', + hotkeys: [ + { key: 'CTRL+Q', info: 'bosqichlar sahifasidan o\'yinni tark eting' }, + { key: 'KEY NUMBERS', info: 'tegishli bosqichni tanlash' }, + { key: 'KEY D', info: 'o\'yin qiyinligini o\'zgartirish' }, + { key: 'ENTER', info: 'o\'yinni boshlash(bosqichni tanlayotganda)' }, + { key: 'CTRL+M', info: 'bosqichlar sahifasiga o\'tish(o\'yin to\'xtagan vaqtda)' }, + { key: 'ESC', info: 'oynani yopish' }, + { key: 'KEY N', info: 'birinchi to\'lqinni boshlash' }, + { key: 'CTRL+SPACE', info: 'o\'yinni to\'xtatib turish' }, + { key: 'CTRL+LEFT/RIGHT', info: 'g\'alaba/mag\'lubiyat paytidagi tegishli tugma' }, + ], + price: 'narx', + damage: 'zarar', +}; diff --git a/frontend/src/scripts/utils/create.ts b/frontend/src/scripts/utils/create.ts new file mode 100644 index 0000000..b8ef5cc --- /dev/null +++ b/frontend/src/scripts/utils/create.ts @@ -0,0 +1,53 @@ +/** + * @param {String} el + * @param {String} classNames + * @param {HTMLElement} child + * @param {HTMLElement} parent + * @param {...array} dataAttr + */ + +export default function create( + el: string, + classNames: string, + child: any, + parent: Element | null, + ...dataAttr: string[][] +): HTMLElement { + let element: HTMLElement; + + try { + element = document.createElement(el); + } catch (error) { + throw new Error('Unable to create HTMLElement! Give a proper tag name'); + } + + if (classNames) element.classList.add(...classNames.split(' ')); + + if (child && Array.isArray(child)) { + child.forEach((childElement) => childElement && element.append(childElement)); + } else if (child && typeof child === 'object') { + element.append(child); + } else if (child && typeof child === 'string') { + element.innerHTML = child; + } + + if (parent) parent.append(element); + + if (dataAttr.length) { + dataAttr.forEach(([attrName, attrValue]: string[]) => { + if (attrValue === '') element.setAttribute(attrName, ''); + + if ( + attrName.match( + /value|id|placeholder|cols|rows|role|aria|src|alt|title|autocorrect|spellcheck|viewBox/, + ) + ) { + element.setAttribute(attrName, attrValue); + } else { + element.dataset[attrName] = attrValue; + } + }); + } + + return element; +} diff --git a/frontend/src/scripts/utils/getRandomDeviationWay.ts b/frontend/src/scripts/utils/getRandomDeviationWay.ts new file mode 100644 index 0000000..b5ced52 --- /dev/null +++ b/frontend/src/scripts/utils/getRandomDeviationWay.ts @@ -0,0 +1,5 @@ +import { RANDOM_WAY_COEFFICIENT } from '../constants/constants'; + +export default function getRandomDeviationWay(): number { + return ((Math.random() < 0.5) ? -1 : 1) * (RANDOM_WAY_COEFFICIENT + Math.random()); +} diff --git a/frontend/src/styles/style.scss b/frontend/src/styles/style.scss new file mode 100644 index 0000000..6477dea --- /dev/null +++ b/frontend/src/styles/style.scss @@ -0,0 +1,21 @@ +@import '../scripts/auth/scss/auth.scss'; +@import '../scripts/achievements/scss/achievements.scss'; +@import '../scripts/credits/scss/credits.scss'; +@import '../scripts/auth/scss/lang-switcher.scss'; + +@font-face { + font-family: "Dimbo"; + src: url("../assets/fonts/Dimbo-ru-en.otf") format("opentype"); +} +.fontPrelod { + font-family: "Dimbo"; + position: absolute; + left: -300px; +} + +canvas { + position: fixed; + top: 0; + z-index: 100; +} +// @import './levels-map.scss'; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1443627 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,56 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "strict": true, + "noImplicitAny": false, + "esModuleInterop": true, + "allowUnreachableCode": false, + "sourceMap": true, + "strictPropertyInitialization": false, + "allowJs": true, + "lib": [ + "dom", + "es6", + "esnext", + "es2015" + ], + "outDir": "./dist/", + }, + "include": [ + "./src/**/*.ts", + "src/scripts/components/unit/Enemy.ts" + ], + "typeRoots": [ + "./node_modules/phaser/types" + ], + "types": [ + "Phaser" + ] +} +// { +// "compileOnSave": true, +// "compilerOptions": { +// "outDir": "./built", +// "allowJs": true, +// "target": "es6", +// "allowUnreachableCode": false, +// "noImplicitReturns": true, +// "noImplicitAny": true, +// "typeRoots": [ "./typings", "node_modules/@types"], +// "outFile": "./built/combined.js", +// "skipLibCheck": true, +// "strictNullChecks": true +// }, +// "include": [ +// "./**/*" +// ], +// "exclude": [ +// "./plugins/**/*", +// "./typings/**/*", +// "./built/**/*", +// "./node_modules/", +// "node_modules", +// "*/node_modules/" +// ] +// } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js new file mode 100644 index 0000000..835642a --- /dev/null +++ b/frontend/webpack.config.js @@ -0,0 +1,188 @@ +const path = require('path'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const ENV = process.env.npm_lifecycle_event; +const isDev = ENV === 'dev'; +const isProd = ENV === 'build'; + +function setDevTool() { + if (isDev) { + return 'source-map'; + } + return 'none'; +} + +function setDMode() { + if (isProd) { + return 'production'; + } + return 'development'; +} + +const config = { + target: 'web', + entry: { index: './src/scripts/App.ts' }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, + mode: setDMode(), + devtool: setDevTool(), + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [{ + test: /\.html$/, + use: [{ + loader: 'html-loader', + options: { + minimize: false, + }, + }], + }, + // { + // test: /\.js$/, + // use: ['babel-loader'/* , 'eslint-loader' */], + // exclude: [ + // /node_modules/, + // ], + // }, + { + test: /\.ts$|\.js$/, + loader: 'ts-loader', + include: path.join(__dirname, './src'), + exclude: [ + /node_modules/, + ], + }, + { + test: /\.css$/, + use: [ + 'style-loader', + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.scss$/, + use: [ + 'style-loader', + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + sourceMap: true, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.(jpe?g|png|svg|gif)$/, + use: [ + { + loader: 'file-loader', + options: { + outputPath: 'assets/imgs', + name: '[name].[ext]', + esModule: false, + }, + }, + { + loader: 'image-webpack-loader', + options: { + bypassOnDebug: true, + mozjpeg: { + progressive: true, + quality: 75, + }, + // optipng.enabled: false will disable optipng + optipng: { + enabled: false, + }, + pngquant: { + quality: [0.65, 0.90], + speed: 4, + }, + gifsicle: { + interlaced: false, + optimizationLevel: 1, + }, + // the webp option will enable WEBP + webp: { + quality: 75, + }, + }, + }, + ], + }, + { + test: /\.(woff|woff2|ttf|otf|eot)$/, + use: [{ + loader: 'file-loader', + options: { + outputPath: 'fonts', + }, + }], + }, + { + test: /\.(mp3)$/, + use: [{ + loader: 'file-loader', + options: { + outputPath: 'assets', + }, + }], + }, + ], + }, + + plugins: [ + new MiniCssExtractPlugin({ + filename: 'styles.css', + }), + new HtmlWebPackPlugin({ + template: './src/index.html', + filename: './index.html', + }), + new CopyWebpackPlugin([ + { from: './src/assets/', to: './assets/' }, + { from: './src/favicon.png' }, + { + from: path.resolve(__dirname, 'src/favicon.png'), + to: path.resolve(__dirname, 'dist'), + }, + ]), + ], + + devServer: { + contentBase: path.join(__dirname, 'dist'), + compress: true, + port: 3000, + overlay: true, + stats: 'errors-only', + clientLogLevel: 'none', + }, +}; + +if (isProd) { + config.plugins.push( + new UglifyJSPlugin(), + ); +} + +module.exports = config;