├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy-nf.yml │ ├── deploy-snek-test.yml │ ├── deploy-snek.yml │ ├── maven.yml │ ├── show-logs-nf.yml │ ├── show-logs-snek.yml │ ├── sonar-backend.yml │ └── sonar-frontend.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── backend ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── click │ │ │ └── snekhome │ │ │ └── backend │ │ │ ├── BackendApplication.java │ │ │ ├── Config.java │ │ │ ├── WebSocketConfig.java │ │ │ ├── controller │ │ │ ├── MapboxController.java │ │ │ ├── NodeController.java │ │ │ ├── PlayerController.java │ │ │ └── WebSocketController.java │ │ │ ├── exception │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── NoSuchPlayerException.java │ │ │ ├── UsernameAlreadyExistsException.java │ │ │ └── WrongRoleException.java │ │ │ ├── model │ │ │ ├── ChatMessage.java │ │ │ ├── Coordinates.java │ │ │ ├── CustomPlacesResponse.java │ │ │ ├── CustomPlacesResult.java │ │ │ ├── Geometry.java │ │ │ ├── Modifier.java │ │ │ ├── Node.java │ │ │ ├── NodeData.java │ │ │ └── Player.java │ │ │ ├── repo │ │ │ ├── NodeRepo.java │ │ │ └── PlayerRepo.java │ │ │ ├── security │ │ │ ├── CustomAuthenticationEntryPoint.java │ │ │ ├── MongoUser.java │ │ │ ├── MongoUserController.java │ │ │ ├── MongoUserDetailService.java │ │ │ ├── MongoUserRepository.java │ │ │ ├── MongoUserService.java │ │ │ ├── Role.java │ │ │ ├── SecurityConfig.java │ │ │ ├── UserData.java │ │ │ └── UserWithoutId.java │ │ │ ├── service │ │ │ ├── GooglePlacesService.java │ │ │ ├── NodeService.java │ │ │ └── PlayerService.java │ │ │ └── util │ │ │ ├── ActionType.java │ │ │ ├── Calculation.java │ │ │ ├── IdService.java │ │ │ ├── ItemSize.java │ │ │ ├── NodeFunctions.java │ │ │ └── PlayerFunctions.java │ └── resources │ │ └── application.properties │ └── test │ ├── java │ └── click │ │ └── snekhome │ │ └── backend │ │ ├── BackendApplicationTests.java │ │ ├── ConfigTest.java │ │ ├── controller │ │ ├── IntegrationTest.java │ │ ├── MapboxControllerTest.java │ │ └── WebSocketControllerUnitTest.java │ │ ├── security │ │ ├── MongoUserDetailServiceTest.java │ │ └── MongoUserServiceTest.java │ │ ├── service │ │ ├── NodeServiceTest.java │ │ └── PlayerServiceTest.java │ │ └── util │ │ ├── IdServiceTest.java │ │ ├── NodeFunctionsTest.java │ │ └── PlayerFunctionsTest.java │ └── resources │ ├── application.properties │ └── static │ ├── existing-resource.txt │ ├── index.html │ └── response.png ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── netrunner.png ├── src │ ├── App.tsx │ ├── GlobalStyle.tsx │ ├── assets │ │ ├── fonts │ │ │ ├── 3270 │ │ │ │ └── 3270NerdFont-Regular.ttf │ │ │ └── Cyberpunk │ │ │ │ └── Cyberpunk.ttf │ │ ├── images │ │ │ ├── avatar.png │ │ │ ├── defaultAvatar.webp │ │ │ ├── defaultAvatar_background.webp │ │ │ ├── intelligence.png │ │ │ └── intelligence_background.png │ │ ├── sounds │ │ │ ├── click.mp3 │ │ │ ├── electric_machine.mp3 │ │ │ ├── error.mp3 │ │ │ ├── key_press.mp3 │ │ │ ├── loading_os.mp3 │ │ │ ├── login_success.mp3 │ │ │ ├── switch.mp3 │ │ │ ├── upgrade.mp3 │ │ │ ├── zoom_in.mp3 │ │ │ └── zoom_out.mp3 │ │ └── svg │ │ │ ├── cctv_enemy.svg │ │ │ ├── cctv_neutral.svg │ │ │ ├── cctv_player.svg │ │ │ ├── character_enemy.svg │ │ │ ├── character_player.svg │ │ │ ├── cpu_enemy.svg │ │ │ ├── cpu_neutral.svg │ │ │ ├── cpu_player.svg │ │ │ ├── databanse_player.svg │ │ │ ├── database_enemy.svg │ │ │ ├── database_neutral.svg │ │ │ ├── layers.svg │ │ │ ├── padlock_active.svg │ │ │ ├── padlock_neutral.svg │ │ │ ├── position.svg │ │ │ ├── radio_enemy.svg │ │ │ ├── radio_neutral.svg │ │ │ ├── radio_player.svg │ │ │ ├── talk.svg │ │ │ ├── trading_enemy.svg │ │ │ ├── trading_neutral.svg │ │ │ └── trading_player.svg │ ├── components │ │ ├── ActionButton.tsx │ │ ├── AddButton.tsx │ │ ├── AddPage.tsx │ │ ├── ChatMessage.tsx │ │ ├── ChatPage.tsx │ │ ├── ChatViewButton.tsx │ │ ├── CooldownCounter.tsx │ │ ├── GpsButton.tsx │ │ ├── HealthBar.tsx │ │ ├── LoginPage.tsx │ │ ├── MapView.tsx │ │ ├── MiniActionButton.tsx │ │ ├── NavBar.tsx │ │ ├── NodeFilter.tsx │ │ ├── NodeItem.tsx │ │ ├── NodeList.tsx │ │ ├── PlayerInfoBar.tsx │ │ ├── PlayerPage.tsx │ │ ├── PlayerPopup.tsx │ │ ├── ProtectedRoutes.tsx │ │ ├── RechargingButton.tsx │ │ ├── SettingsPage.tsx │ │ ├── StatusBar.tsx │ │ ├── StorePage.tsx │ │ ├── ViewChangeButton.tsx │ │ ├── VolumeBar.tsx │ │ ├── css │ │ │ └── leaflet.css │ │ ├── icons │ │ │ ├── AttackIcon.tsx │ │ │ ├── CharacterIcon.tsx │ │ │ ├── ChatIcon.tsx │ │ │ ├── CpuIcon.tsx │ │ │ ├── DowngradeIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── MiniAttackIcon.tsx │ │ │ ├── MiniDowngradeIcon.tsx │ │ │ ├── MiniUpgradeIcon.tsx │ │ │ ├── NavigationIcon.tsx │ │ │ ├── NetworkIcon.tsx │ │ │ ├── PositionIcon.tsx │ │ │ ├── ScanIcon.tsx │ │ │ ├── SoundIcon.tsx │ │ │ ├── UnlockIcon.tsx │ │ │ ├── UpgradeIcon.tsx │ │ │ └── mapIcons.ts │ │ └── styled │ │ │ ├── StyledButtonContainer.ts │ │ │ ├── StyledForm.ts │ │ │ ├── StyledFormButton.ts │ │ │ ├── StyledHelperContainer.ts │ │ │ ├── StyledHelperText.ts │ │ │ ├── StyledInput.ts │ │ │ ├── StyledLabel.ts │ │ │ ├── StyledStatusBar.ts │ │ │ └── StyledToastContainer.ts │ ├── hooks │ │ ├── useCooldown.ts │ │ ├── useNodes.ts │ │ ├── useOwner.ts │ │ ├── usePlayer.ts │ │ └── useStore.ts │ ├── index.css │ ├── main.tsx │ ├── models.ts │ ├── theme.ts │ ├── utils │ │ ├── calculation.ts │ │ ├── getDistanceString.ts │ │ └── sound.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── pull_request_template.md └── sonar-project.properties /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: toshydev 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy-nf.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy App on NF" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-frontend: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '18' 16 | - name: Build Frontend 17 | run: | 18 | cd frontend 19 | npm install 20 | npm run build 21 | - uses: actions/upload-artifact@v2 22 | with: 23 | name: frontend-build 24 | path: frontend/dist/ 25 | build-backend: 26 | runs-on: ubuntu-latest 27 | needs: build-frontend 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Set up JDK 32 | uses: actions/setup-java@v2 33 | with: 34 | #Set Java Version 35 | java-version: '20' 36 | distribution: 'adopt' 37 | cache: 'maven' 38 | - uses: actions/download-artifact@v2 39 | with: 40 | name: frontend-build 41 | path: backend/src/main/resources/static 42 | 43 | - name: Build with maven 44 | run: mvn -B package --file backend/pom.xml 45 | 46 | - uses: actions/upload-artifact@v2 47 | with: 48 | name: app.jar 49 | path: backend/target/netrunner.jar 50 | 51 | push-to-docker-hub: 52 | runs-on: ubuntu-latest 53 | needs: build-backend 54 | steps: 55 | - uses: actions/checkout@v2 56 | 57 | - uses: actions/download-artifact@v2 58 | with: 59 | name: app.jar 60 | path: backend/target 61 | 62 | - name: Login to DockerHub 63 | uses: docker/login-action@v1 64 | with: 65 | #Set dockerhub username 66 | username: toshymoshy 67 | password: ${{ secrets.DOCKER_PASSWORD }} 68 | 69 | - name: Build and push 70 | uses: docker/build-push-action@v2 71 | with: 72 | push: true 73 | #Set dockerhub username/projectName 74 | tags: toshymoshy/netrunner 75 | context: . 76 | 77 | deploy: 78 | runs-on: ubuntu-latest 79 | needs: push-to-docker-hub 80 | steps: 81 | - name: Restart docker container 82 | uses: appleboy/ssh-action@master 83 | with: 84 | host: capstone-project.de 85 | #Set App Name (replace "example" with "alpha"-"tango") 86 | username: cgn-java-23-2-anton 87 | password: ${{ secrets.SSH_PASSWORD_NF }} 88 | #Set App Name (replace "example" with "alpha"-"tango") 89 | #Set dockerhub project (replace "bartfastiel/java-capstone-project.de-example-app") 90 | #Set IP (replace "10.0.1.99" with "10.0.1.1"-"10.0.1.20") 91 | script: | 92 | sudo docker stop cgn-java-23-2-anton 93 | sudo docker rm cgn-java-23-2-anton 94 | sudo docker run --pull=always --name cgn-java-23-2-anton --network capstones --ip 10.0.5.3 --restart always --detach --env MONGO_DB_URI="${{ secrets.MONGO_DB_URI }}" --env MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" --env GOOGLE_API_KEY="${{ secrets.GOOGLE_API_KEY }}" toshymoshy/netrunner:latest 95 | sleep 15s 96 | sudo docker logs cgn-java-23-2-anton 97 | 98 | - name: Check the deployed service URL 99 | uses: jtalk/url-health-check-action@v3 100 | with: 101 | #Set App Name (replace "example" with "alpha"-"tango") 102 | url: http://cgn-java-23-2-anton.capstone-project.de 103 | max-attempts: 3 104 | retry-delay: 5s 105 | retry-all: true 106 | -------------------------------------------------------------------------------- /.github/workflows/deploy-snek-test.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy Test-App on SNEK" 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature 7 | 8 | jobs: 9 | build-frontend: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '18' 16 | - name: Build Frontend 17 | run: | 18 | cd frontend 19 | npm install 20 | npm run build 21 | - uses: actions/upload-artifact@v2 22 | with: 23 | name: frontend-build 24 | path: frontend/dist/ 25 | build-backend: 26 | runs-on: ubuntu-latest 27 | needs: build-frontend 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Set up JDK 32 | uses: actions/setup-java@v2 33 | with: 34 | java-version: '20' 35 | distribution: 'adopt' 36 | cache: 'maven' 37 | - uses: actions/download-artifact@v2 38 | with: 39 | name: frontend-build 40 | path: backend/src/main/resources/static 41 | 42 | - name: Build with maven 43 | run: mvn -B package --file backend/pom.xml 44 | 45 | - uses: actions/upload-artifact@v2 46 | with: 47 | name: app.jar 48 | path: backend/target/netrunner.jar 49 | 50 | push-to-docker-hub: 51 | runs-on: ubuntu-latest 52 | needs: build-backend 53 | steps: 54 | - uses: actions/checkout@v2 55 | 56 | - uses: actions/download-artifact@v2 57 | with: 58 | name: app.jar 59 | path: backend/target 60 | 61 | - name: Login to DockerHub 62 | uses: docker/login-action@v1 63 | with: 64 | username: toshymoshy 65 | password: ${{ secrets.DOCKER_PASSWORD }} 66 | - name: Set up Docker Buildx 67 | uses: docker/setup-buildx-action@v2 68 | - name: Build and push 69 | uses: docker/build-push-action@v2 70 | with: 71 | push: true 72 | platforms: linux/arm64 73 | tags: toshymoshy/netrunner:aarch64-test 74 | context: . 75 | 76 | deploy: 77 | runs-on: ubuntu-latest 78 | needs: push-to-docker-hub 79 | steps: 80 | - name: Restart docker container 81 | uses: appleboy/ssh-action@master 82 | with: 83 | host: snekworld.org 84 | port: ${{ secrets.SSH_PORT_SNEK }} 85 | username: ${{ secrets.SSH_USERNAME_SNEK }} 86 | password: ${{ secrets.SSH_PASSWORD_SNEK }} 87 | script: | 88 | docker stop netrunner-test 89 | docker rm netrunner-test 90 | docker run --pull=always --name netrunner-test -p 8081:8080 --restart always --detach --env MONGO_DB_URI="${{ secrets.MONGO_DB_URI_TEST }}" --env MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" --env GOOGLE_API_KEY="${{ secrets.GOOGLE_API_KEY }}" toshymoshy/netrunner:aarch64-test 91 | sleep 15s 92 | docker logs netrunner-test 93 | 94 | - name: Check the deployed service URL 95 | uses: jtalk/url-health-check-action@v3 96 | with: 97 | url: http://test.snekworld.org 98 | max-attempts: 3 99 | retry-delay: 5s 100 | retry-all: true 101 | -------------------------------------------------------------------------------- /.github/workflows/deploy-snek.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy App on SNEK" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-frontend: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '18' 16 | - name: Build Frontend 17 | run: | 18 | cd frontend 19 | npm install 20 | npm run build 21 | - uses: actions/upload-artifact@v2 22 | with: 23 | name: frontend-build 24 | path: frontend/dist/ 25 | build-backend: 26 | runs-on: ubuntu-latest 27 | needs: build-frontend 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Set up JDK 32 | uses: actions/setup-java@v2 33 | with: 34 | java-version: '20' 35 | distribution: 'adopt' 36 | cache: 'maven' 37 | - uses: actions/download-artifact@v2 38 | with: 39 | name: frontend-build 40 | path: backend/src/main/resources/static 41 | 42 | - name: Build with maven 43 | run: mvn -B package --file backend/pom.xml 44 | 45 | - uses: actions/upload-artifact@v2 46 | with: 47 | name: app.jar 48 | path: backend/target/netrunner.jar 49 | 50 | push-to-docker-hub: 51 | runs-on: ubuntu-latest 52 | needs: build-backend 53 | steps: 54 | - uses: actions/checkout@v2 55 | 56 | - uses: actions/download-artifact@v2 57 | with: 58 | name: app.jar 59 | path: backend/target 60 | 61 | - name: Login to DockerHub 62 | uses: docker/login-action@v1 63 | with: 64 | username: toshymoshy 65 | password: ${{ secrets.DOCKER_PASSWORD }} 66 | - name: Set up Docker Buildx 67 | uses: docker/setup-buildx-action@v2 68 | - name: Build and push 69 | uses: docker/build-push-action@v2 70 | with: 71 | push: true 72 | platforms: linux/arm64 73 | tags: toshymoshy/netrunner:aarch64 74 | context: . 75 | 76 | deploy: 77 | runs-on: ubuntu-latest 78 | needs: push-to-docker-hub 79 | steps: 80 | - name: Restart docker container 81 | uses: appleboy/ssh-action@master 82 | with: 83 | host: snekworld.org 84 | port: ${{ secrets.SSH_PORT_SNEK }} 85 | username: ${{ secrets.SSH_USERNAME_SNEK }} 86 | password: ${{ secrets.SSH_PASSWORD_SNEK }} 87 | script: | 88 | docker stop netrunner 89 | docker rm netrunner 90 | docker run --pull=always --name netrunner -p 8080:8080 --restart always --detach --env MONGO_DB_URI="${{ secrets.MONGO_DB_URI }}" --env MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" --env GOOGLE_API_KEY="${{ secrets.GOOGLE_API_KEY }}" toshymoshy/netrunner:aarch64 91 | sleep 15s 92 | docker logs netrunner 93 | 94 | - name: Check the deployed service URL 95 | uses: jtalk/url-health-check-action@v3 96 | with: 97 | url: http://snekworld.org 98 | max-attempts: 3 99 | retry-delay: 5s 100 | retry-all: true 101 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 20 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '20' 25 | distribution: 'temurin' 26 | cache: maven 27 | - name: Build with Maven 28 | run: mvn -B package --file backend/pom.xml 29 | -------------------------------------------------------------------------------- /.github/workflows/show-logs-nf.yml: -------------------------------------------------------------------------------- 1 | name: "Get Logs from NF" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | get-logs: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get logs from docker 11 | uses: appleboy/ssh-action@master 12 | with: 13 | host: capstone-project.de 14 | #Set App Name ("alpha" - "tango") 15 | username: cgn-java-23-2-anton 16 | password: ${{ secrets.SSH_PASSWORD_NF }} 17 | #Set App Name ("alpha" - "tango") 18 | script: | 19 | sudo docker logs cgn-java-23-2-anton 20 | - name: Check the deployed service URL 21 | uses: jtalk/url-health-check-action@v3 22 | with: 23 | #Set App Name ("alpha" - "tango") 24 | url: http://cgn-java-23-2-anton.capstone-project.de 25 | max-attempts: 3 26 | retry-delay: 5s 27 | retry-all: true 28 | -------------------------------------------------------------------------------- /.github/workflows/show-logs-snek.yml: -------------------------------------------------------------------------------- 1 | name: "Get Logs from SNEK" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | get-logs: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get logs from docker 11 | uses: appleboy/ssh-action@master 12 | with: 13 | host: snekworld.org 14 | port: ${{ secrets.SSH_PORT_SNEK }} 15 | username: ${{ secrets.SSH_USERNAME_SNEK }} 16 | password: ${{ secrets.SSH_PASSWORD_SNEK }} 17 | script: | 18 | docker logs netrunner 19 | - name: Check the deployed service URL 20 | uses: jtalk/url-health-check-action@v3 21 | with: 22 | url: http://snekworld.org 23 | max-attempts: 3 24 | retry-delay: 5s 25 | retry-all: true 26 | -------------------------------------------------------------------------------- /.github/workflows/sonar-backend.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | build: 10 | name: Build and analyze 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 16 | - name: Set up JDK 20 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: 20 20 | distribution: 'zulu' # Alternative distribution options are available. 21 | - name: Cache SonarCloud packages 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.sonar/cache 25 | key: ${{ runner.os }}-sonar 26 | restore-keys: ${{ runner.os }}-sonar 27 | - name: Cache Maven packages 28 | uses: actions/cache@v3 29 | with: 30 | path: ~/.m2 31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 32 | restore-keys: ${{ runner.os }}-m2 33 | - name: Build and analyze 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 37 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=netRunner_backend -file backend/pom.xml 38 | -------------------------------------------------------------------------------- /.github/workflows/sonar-frontend.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | sonarcloud: 10 | name: SonarCloud 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 16 | - name: SonarCloud Scan 17 | uses: SonarSource/sonarcloud-github-action@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 20 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.iml 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:20 2 | ENV ENVIRONMENT=prod 3 | LABEL maintainer="github.com/toshydev" 4 | EXPOSE 8080 5 | ADD backend/target/netrunner.jar app.jar 6 | CMD [ "sh", "-c", "java -jar /app.jar" ] -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest version of **Netrunner** is currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 0.0.1 | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Use the [Netrunner Discussions](https://github.com/toshydev/netrunner/discussions) to report a vulnerability or search for reported vulnerabilities. 14 | If your report is accepted it will be handled in the next update. 15 | If your report is declined you can request an explanation by the provided contact email in the [Code Of Conduct section](https://github.com/toshydev/netrunner/blob/main/CODE_OF_CONDUCT.md) 16 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /backend/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/backend/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /backend/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /backend/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/BackendApplication.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | @SpringBootApplication 9 | public class BackendApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(BackendApplication.class, args); 13 | } 14 | 15 | @Bean 16 | public RestTemplate restTemplate() { 17 | return new RestTemplate(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/Config.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.core.io.ClassPathResource; 5 | import org.springframework.core.io.Resource; 6 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | import org.springframework.web.servlet.resource.PathResourceResolver; 9 | 10 | import java.io.IOException; 11 | 12 | @Configuration 13 | public class Config implements WebMvcConfigurer { 14 | @Override 15 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 16 | registry.addResourceHandler("/**").addResourceLocations("classpath:/static/").resourceChain(true).addResolver(new PathResourceResolver() { 17 | @Override 18 | protected Resource getResource(String resourcePath, Resource location) throws IOException { 19 | Resource requestedResource = location.createRelative(resourcePath); 20 | 21 | return (requestedResource.exists() && requestedResource.isReadable()) ? requestedResource : new ClassPathResource("/static/index.html"); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend; 2 | 3 | import click.snekhome.backend.controller.WebSocketController; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 6 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 7 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 8 | 9 | @EnableWebSocket 10 | @Configuration 11 | public class WebSocketConfig implements WebSocketConfigurer { 12 | 13 | private final WebSocketController webSocketController; 14 | 15 | public WebSocketConfig(WebSocketController webSocketController) { 16 | this.webSocketController = webSocketController; 17 | } 18 | @Override 19 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 20 | registry.addHandler(webSocketController, "/api/ws/chat").setAllowedOrigins("*"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/controller/MapboxController.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | @RestController 15 | @RequestMapping("/api/map") 16 | public class MapboxController { 17 | 18 | private final RestTemplate restTemplate; 19 | 20 | public MapboxController(RestTemplate restTemplate) { 21 | this.restTemplate = restTemplate; 22 | } 23 | 24 | @Value("${mapbox.access.token}") 25 | private String mapboxAccessToken; 26 | 27 | @GetMapping("/{z}/{x}/{y}") 28 | public ResponseEntity getImageWithResponseEntity(@PathVariable int z, @PathVariable int x, @PathVariable int y) { 29 | 30 | String mapboxApiUrl = "https://api.mapbox.com/styles/v1/antonroters/cll3ohya000fg01pl7cc9fuu8"; 31 | String fullUrl = mapboxApiUrl + "/tiles/256/" + z + "/" + x + "/" + y + "@2x?access_token=" + mapboxAccessToken; 32 | 33 | HttpHeaders headers = new HttpHeaders(); 34 | headers.setContentType(MediaType.IMAGE_PNG); 35 | HttpEntity entity = new HttpEntity<>(headers); 36 | 37 | return restTemplate.getForEntity(fullUrl, byte[].class, entity); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/controller/NodeController.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import click.snekhome.backend.model.*; 4 | import click.snekhome.backend.service.NodeService; 5 | import click.snekhome.backend.util.ActionType; 6 | import jakarta.validation.Valid; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/api/nodes") 15 | public class NodeController { 16 | 17 | private final NodeService nodeService; 18 | 19 | public NodeController(NodeService nodeService) { 20 | this.nodeService = nodeService; 21 | } 22 | 23 | @ResponseStatus(HttpStatus.OK) 24 | @GetMapping 25 | public List getNodes() { 26 | return this.nodeService.list(); 27 | } 28 | 29 | @ResponseStatus(HttpStatus.OK) 30 | @GetMapping("{id}") 31 | public List getNodesByOwner(@PathVariable String id) { 32 | return this.nodeService.getNodesByOwner(id); 33 | } 34 | 35 | @ResponseStatus(HttpStatus.CREATED) 36 | @PostMapping 37 | public Node add(@Valid @RequestBody NodeData nodeData) { 38 | return this.nodeService.add(nodeData); 39 | } 40 | 41 | @ResponseStatus(HttpStatus.ACCEPTED) 42 | @PutMapping("{id}") 43 | public Node edit(@PathVariable String id, @RequestBody String actionType) { 44 | return this.nodeService.edit(id, ActionType.valueOf(actionType)); 45 | } 46 | 47 | @ResponseStatus(HttpStatus.ACCEPTED) 48 | @DeleteMapping("{id}") 49 | public void delete(@PathVariable String id) { 50 | this.nodeService.delete(id); 51 | } 52 | 53 | @ResponseStatus(HttpStatus.CREATED) 54 | @PostMapping("scan") 55 | public List scan(@RequestBody Coordinates coordinates) throws IOException { 56 | return this.nodeService.scan(coordinates); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/controller/PlayerController.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import click.snekhome.backend.model.Coordinates; 4 | import click.snekhome.backend.model.Player; 5 | import click.snekhome.backend.service.PlayerService; 6 | import click.snekhome.backend.util.ItemSize; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/api/player") 15 | public class PlayerController { 16 | private final PlayerService playerService; 17 | 18 | public PlayerController(PlayerService playerService) { 19 | this.playerService = playerService; 20 | } 21 | 22 | @ResponseStatus(HttpStatus.OK) 23 | @GetMapping 24 | public Player getPlayer() { 25 | String username = SecurityContextHolder 26 | .getContext() 27 | .getAuthentication() 28 | .getName(); 29 | return playerService.getPlayer(username); 30 | } 31 | 32 | @ResponseStatus(HttpStatus.OK) 33 | @GetMapping("{id}") 34 | public String getPlayerName(@PathVariable String id) { 35 | return playerService.getPlayerNameById(id); 36 | } 37 | 38 | @ResponseStatus(HttpStatus.OK) 39 | @GetMapping("info/{name}") 40 | public Player getPlayerByName(@PathVariable String name) { 41 | return playerService.getPlayer(name); 42 | } 43 | 44 | @ResponseStatus(HttpStatus.ACCEPTED) 45 | @PutMapping("location") 46 | public Player updateLocation(@RequestBody Coordinates coordinates) { 47 | return playerService.updateLocation(coordinates); 48 | } 49 | 50 | @ResponseStatus(HttpStatus.OK) 51 | @GetMapping("enemies") 52 | public List getEnemies() { 53 | return this.playerService.getEnemies(); 54 | } 55 | 56 | @ResponseStatus(HttpStatus.ACCEPTED) 57 | @PutMapping("store") 58 | public Player buyItem(@RequestBody String itemSize) { 59 | return this.playerService.buyItem(ItemSize.valueOf(itemSize)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/controller/WebSocketController.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import click.snekhome.backend.model.ChatMessage; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.web.socket.CloseStatus; 8 | import org.springframework.web.socket.TextMessage; 9 | import org.springframework.web.socket.WebSocketSession; 10 | import org.springframework.web.socket.handler.TextWebSocketHandler; 11 | 12 | import java.io.IOException; 13 | import java.time.Instant; 14 | import java.util.HashSet; 15 | import java.util.Objects; 16 | import java.util.Set; 17 | 18 | @Service 19 | public class WebSocketController extends TextWebSocketHandler { 20 | 21 | final Set activeSessions = new HashSet<>(); 22 | final Set activeUsers = new HashSet<>(); 23 | private final ObjectMapper objectMapper = new ObjectMapper(); 24 | 25 | @Override 26 | public void afterConnectionEstablished(@NotNull WebSocketSession session) throws Exception { 27 | String username = Objects.requireNonNull(session.getPrincipal()).getName(); 28 | String message = username + " is online"; 29 | broadcastMessage(message); 30 | activeSessions.add(session); 31 | welcomeUser(session); 32 | activeUsers.add(username); 33 | sendActiveUsers(); 34 | } 35 | 36 | @Override 37 | protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 38 | String username = Objects.requireNonNull(session.getPrincipal()).getName(); 39 | ChatMessage chatMessage = new ChatMessage(username, message.getPayload(), Instant.now().getEpochSecond()); 40 | TextMessage textMessage = new TextMessage(chatMessage.toString()); 41 | for (WebSocketSession activeSession : activeSessions) { 42 | activeSession.sendMessage(textMessage); 43 | } 44 | } 45 | 46 | @Override 47 | public void afterConnectionClosed(@NotNull WebSocketSession session, CloseStatus status) throws Exception { 48 | String username = Objects.requireNonNull(session.getPrincipal()).getName(); 49 | activeSessions.remove(session); 50 | activeUsers.remove(username); 51 | String message = username + " disconnected"; 52 | broadcastMessage(message); 53 | sendActiveUsers(); 54 | } 55 | 56 | void broadcastMessage(String message) throws IOException { 57 | ChatMessage chatMessage = new ChatMessage("Netwalker", message, Instant.now().getEpochSecond()); 58 | TextMessage textMessage = new TextMessage(chatMessage.toString()); 59 | for (WebSocketSession activeSession : activeSessions) { 60 | activeSession.sendMessage(textMessage); 61 | } 62 | } 63 | 64 | void welcomeUser(WebSocketSession session) throws IOException { 65 | String serverMessage = "Welcome to Netwalker! You are now connected to the new chat server. Join us on Discord: https://discord.gg/CmtvmcbPCq"; 66 | ChatMessage welcomeMessage = new ChatMessage("Netwalker", serverMessage, Instant.now().getEpochSecond()); 67 | TextMessage welcomeTextMessage = new TextMessage(welcomeMessage.toString()); 68 | session.sendMessage(welcomeTextMessage); 69 | } 70 | 71 | void sendActiveUsers() throws IOException { 72 | String json = objectMapper.writeValueAsString(activeUsers); 73 | TextMessage message = new TextMessage(json); 74 | for (WebSocketSession activeSession : activeSessions) { 75 | activeSession.sendMessage(message); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.MethodArgumentNotValidException; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | import org.springframework.web.bind.annotation.RestControllerAdvice; 8 | 9 | @RestControllerAdvice 10 | public class GlobalExceptionHandler { 11 | 12 | @ExceptionHandler({MethodArgumentNotValidException.class}) 13 | @ResponseStatus(HttpStatus.BAD_REQUEST) 14 | public String handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { 15 | return exception.getAllErrors().get(0).getDefaultMessage(); 16 | } 17 | 18 | @ExceptionHandler({UsernameAlreadyExistsException.class}) 19 | @ResponseStatus(HttpStatus.FORBIDDEN) 20 | public String handleUsernameAlreadyExistsException(UsernameAlreadyExistsException exception) { 21 | return exception.getMessage(); 22 | } 23 | 24 | @ExceptionHandler({WrongRoleException.class}) 25 | @ResponseStatus(HttpStatus.FORBIDDEN) 26 | public String handleWrongRoleException(WrongRoleException exception) { 27 | return exception.getMessage(); 28 | } 29 | 30 | @ExceptionHandler({NoSuchPlayerException.class}) 31 | @ResponseStatus(HttpStatus.NOT_FOUND) 32 | public String handleNoSuchPlayerException(NoSuchPlayerException exception) { 33 | return exception.getMessage(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/exception/NoSuchPlayerException.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.exception; 2 | 3 | public class NoSuchPlayerException extends RuntimeException{ 4 | public NoSuchPlayerException(String message) { 5 | super("Player " + message + " not found!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/exception/UsernameAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "Username already exists!") 7 | public class UsernameAlreadyExistsException extends RuntimeException{ 8 | public UsernameAlreadyExistsException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/exception/WrongRoleException.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "Wrong role!") 7 | public class WrongRoleException extends RuntimeException{ 8 | public WrongRoleException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | public record ChatMessage(String username, String message, long timestamp) { 4 | @Override 5 | public String toString() { 6 | return "{\"username\":\"" + username + "\",\"message\":\"" + message + "\",\"timestamp\":" + timestamp + "}"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/Coordinates.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import jakarta.validation.constraints.Max; 4 | import jakarta.validation.constraints.Min; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | public record Coordinates( 8 | @NotNull(message = "Latitude cannot be null") 9 | @Max(value = 90, message = "Latitude cannot be greater than 90") 10 | @Min(value = -90, message = "Latitude cannot be less than -90") 11 | double latitude, 12 | 13 | @NotNull(message = "Longitude cannot be null") 14 | @Max(value = 180, message = "Longitude cannot be greater than 180") 15 | @Min(value = -180, message = "Longitude cannot be less than -180") 16 | double longitude, 17 | 18 | @NotNull(message = "Timestamp cannot be null") 19 | long timestamp) { 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/CustomPlacesResponse.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import java.util.List; 4 | 5 | public record CustomPlacesResponse ( 6 | List html_attributions, 7 | List results, 8 | String status, 9 | String next_page_token) 10 | {} 11 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/CustomPlacesResult.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.util.List; 6 | 7 | @JsonIgnoreProperties(ignoreUnknown = true, value = "photos") 8 | public record CustomPlacesResult ( 9 | String placeId, 10 | Geometry geometry, 11 | String name, 12 | List types) 13 | {} 14 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/Geometry.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.google.maps.model.LatLng; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public record Geometry ( 8 | LatLng location 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/Modifier.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | public record Modifier( 4 | int experienceModifier, 5 | int attackModifier, 6 | int creditsModifier 7 | ) { 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/Node.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document("nodes") 7 | public record Node( 8 | @Id 9 | String id, 10 | String ownerId, 11 | String name, 12 | int level, 13 | int health, 14 | Coordinates coordinates, 15 | long lastUpdate, 16 | long lastAttack) { 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/NodeData.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import jakarta.validation.Valid; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | 8 | public record NodeData( 9 | @NotBlank(message = "Name cannot be blank") 10 | @Size(max = 15, message = "Name cannot be longer than 15 characters") 11 | String name, 12 | @NotNull(message = "Coordinates cannot be null") 13 | @Valid 14 | Coordinates coordinates) { 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/model/Player.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document("players") 7 | public record Player( 8 | @Id 9 | String id, 10 | String userId, 11 | String name, 12 | Coordinates coordinates, 13 | int level, 14 | int experience, 15 | int experienceToNextLevel, 16 | int health, 17 | int maxHealth, 18 | int attack, 19 | int maxAttack, 20 | int credits, 21 | long lastScan 22 | ) { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/repo/NodeRepo.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.repo; 2 | 3 | import click.snekhome.backend.model.Node; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface NodeRepo extends MongoRepository { 11 | 12 | List findAllByOwnerId(String ownerId); 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/repo/PlayerRepo.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.repo; 2 | 3 | import click.snekhome.backend.model.Player; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface PlayerRepo extends MongoRepository { 12 | Optional findPlayerByName(String name); 13 | 14 | Optional findPlayerByid(String id); 15 | 16 | List findAllByNameIsNot(String name); 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/CustomAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.springframework.security.core.AuthenticationException; 6 | import org.springframework.security.web.AuthenticationEntryPoint; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.io.IOException; 10 | 11 | @Component 12 | public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { 13 | @Override 14 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { 15 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid username or password"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/MongoUser.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document("users") 7 | public record MongoUser( 8 | @Id 9 | String id, 10 | String username, 11 | String email, 12 | String passwordHash, 13 | Role role 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/MongoUserController.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import jakarta.validation.Valid; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | @RestController 14 | @RequestMapping("/api/user") 15 | public class MongoUserController { 16 | 17 | private final MongoUserService mongoUserService; 18 | 19 | public MongoUserController(MongoUserService mongoUserService) { 20 | this.mongoUserService = mongoUserService; 21 | } 22 | 23 | @ResponseStatus(HttpStatus.OK) 24 | @GetMapping 25 | public String getUserData() { 26 | return SecurityContextHolder 27 | .getContext() 28 | .getAuthentication() 29 | .getName(); 30 | } 31 | 32 | @ResponseStatus(HttpStatus.ACCEPTED) 33 | @PostMapping("/login") 34 | public ResponseEntity login() { 35 | String username = SecurityContextHolder 36 | .getContext() 37 | .getAuthentication() 38 | .getName(); 39 | 40 | if (username.equals("anonymousUser")) { 41 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); 42 | } 43 | return ResponseEntity.ok(username); 44 | } 45 | 46 | @ResponseStatus(HttpStatus.CREATED) 47 | @PostMapping("/register") 48 | public void register(@Valid @RequestBody UserWithoutId userWithoutId) { 49 | this.mongoUserService.registerUser(userWithoutId); 50 | } 51 | 52 | @ResponseStatus(HttpStatus.ACCEPTED) 53 | @PostMapping("/logout") 54 | public void logout(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { 55 | SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); 56 | logoutHandler.logout(request, response, authentication); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/MongoUserDetailService.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import org.springframework.security.core.userdetails.User; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Collections; 10 | 11 | @Service 12 | public class MongoUserDetailService implements UserDetailsService { 13 | 14 | private final MongoUserRepository mongoUserRepository; 15 | 16 | public MongoUserDetailService(MongoUserRepository mongoUserRepository) { 17 | this.mongoUserRepository = mongoUserRepository; 18 | } 19 | 20 | @Override 21 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 22 | MongoUser mongoUser = mongoUserRepository.findByUsername(username) 23 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found!")); 24 | 25 | return new User(mongoUser.username(), mongoUser.passwordHash(), Collections.emptyList()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/MongoUserRepository.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.Optional; 7 | 8 | @Repository 9 | public interface MongoUserRepository extends MongoRepository { 10 | Optional findByUsername(String username); 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/MongoUserService.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import click.snekhome.backend.exception.UsernameAlreadyExistsException; 4 | import click.snekhome.backend.service.PlayerService; 5 | import click.snekhome.backend.util.IdService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class MongoUserService { 13 | 14 | private final MongoUserRepository mongoUserRepository; 15 | 16 | private final PlayerService playerService; 17 | 18 | public MongoUserService(MongoUserRepository mongoUserRepository, PlayerService playerService) { 19 | this.mongoUserRepository = mongoUserRepository; 20 | this.playerService = playerService; 21 | } 22 | 23 | public UserData getUserDataByUsername(String username) { 24 | MongoUser mongoUser = mongoUserRepository.findByUsername(username) 25 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found!")); 26 | 27 | return new UserData(mongoUser.id(), mongoUser.username()); 28 | } 29 | 30 | public MongoUser getUserByUsername(String username) { 31 | return mongoUserRepository.findByUsername(username) 32 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found!")); 33 | } 34 | 35 | public void registerUser(UserWithoutId newUser) { 36 | IdService idService = new IdService(); 37 | PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); 38 | String encodedPassword = encoder.encode(newUser.password()); 39 | if (this.mongoUserRepository.findByUsername(newUser.username()).isPresent()) 40 | throw new UsernameAlreadyExistsException("User " + newUser.username() + " already exists!"); 41 | MongoUser user = new MongoUser(idService.generateId(), newUser.username(), newUser.email(), encodedPassword, Role.ADMIN); 42 | this.mongoUserRepository.insert(user); 43 | UserData userData = new UserData(user.id(), user.username()); 44 | this.playerService.createPlayer(userData); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/Role.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | public enum Role { 4 | ADMIN("ADMIN"), 5 | PLAYER("PLAYER"); 6 | 7 | final String type; 8 | 9 | Role(String type) { 10 | this.type = type; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.HttpMethod; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.config.http.SessionCreationPolicy; 9 | import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.security.web.AuthenticationEntryPoint; 12 | import org.springframework.security.web.SecurityFilterChain; 13 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 14 | import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; 15 | 16 | @EnableWebSecurity 17 | @Configuration 18 | public class SecurityConfig { 19 | 20 | private final CustomAuthenticationEntryPoint authenticationEntryPoint; 21 | 22 | public SecurityConfig(CustomAuthenticationEntryPoint authenticationEntryPoint) { 23 | this.authenticationEntryPoint = authenticationEntryPoint; 24 | } 25 | 26 | @Bean 27 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 28 | CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); 29 | requestHandler.setCsrfRequestAttributeName(null); 30 | 31 | return http 32 | .csrf(csrf -> csrf 33 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 34 | .csrfTokenRequestHandler(requestHandler)) 35 | .httpBasic(httpBasic -> httpBasic 36 | .authenticationEntryPoint(authenticationEntryPoint)) 37 | .sessionManagement(httpSecuritySessionManagementConfigurer -> 38 | httpSecuritySessionManagementConfigurer 39 | .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) 40 | .authorizeHttpRequests(httpRequests -> 41 | httpRequests 42 | .requestMatchers(HttpMethod.GET, "/**").permitAll() 43 | .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() 44 | .requestMatchers(HttpMethod.POST, "/api/user/register").permitAll() 45 | .requestMatchers(HttpMethod.POST, "/api/user/logout").permitAll() 46 | .requestMatchers(HttpMethod.GET, "/api/user").permitAll() 47 | .requestMatchers("/api/map").authenticated() 48 | .anyRequest().authenticated()) 49 | .build(); 50 | } 51 | 52 | @Bean 53 | public PasswordEncoder passwordEncoder() { 54 | return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); 55 | } 56 | 57 | @Bean 58 | public AuthenticationEntryPoint authenticationEntryPoint() { 59 | return new CustomAuthenticationEntryPoint(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/UserData.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | public record UserData( 4 | String id, 5 | String username 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/security/UserWithoutId.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Size; 6 | 7 | public record UserWithoutId( 8 | @NotBlank 9 | @Size(min = 3, max = 25, message = "A length between 3 and 15 characters is mandatory.") 10 | String username, 11 | 12 | @Email 13 | String email, 14 | 15 | @NotBlank 16 | //@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!])(?=\\S+$).{8,}$", message = "Password must be at least 8 characters long and contain at least one digit, one lowercase letter, one uppercase letter and one special character") 17 | String password 18 | ) { 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/service/GooglePlacesService.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.service; 2 | 3 | import click.snekhome.backend.model.CustomPlacesResponse; 4 | import click.snekhome.backend.model.CustomPlacesResult; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.google.maps.model.LatLng; 7 | import okhttp3.*; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.io.IOException; 12 | import java.util.*; 13 | import java.util.stream.Stream; 14 | 15 | @Service 16 | public class GooglePlacesService { 17 | 18 | @Value("${google.api.key}") 19 | private String apiKey; 20 | 21 | @Value("${google.api.url}") 22 | private String baseUrl; 23 | private static final String RADIUS = "radius=5000"; 24 | private final OkHttpClient client; 25 | 26 | public GooglePlacesService() { 27 | this.client = new OkHttpClient().newBuilder().build(); 28 | } 29 | 30 | public List getUniquePlaces(String latitude, String longitude) throws IOException { 31 | CustomPlacesResponse atmResponse = this.getPlaces(latitude, longitude, "atm"); 32 | CustomPlacesResponse universityResponse = this.getPlaces(latitude, longitude, "university"); 33 | CustomPlacesResponse shoppingResponse = this.getPlaces(latitude, longitude, "shopping_mall"); 34 | List allPlaces = Stream.concat(shoppingResponse.results().stream(), Stream.concat(atmResponse.results().stream(), universityResponse.results().stream())) 35 | .toList(); 36 | return removeDuplicateLocations(allPlaces); 37 | } 38 | 39 | public CustomPlacesResponse getPlaces (String latitude, String longitude, String type) throws IOException { 40 | ObjectMapper objectMapper = new ObjectMapper(); 41 | 42 | Request request = new Request.Builder() 43 | .url(baseUrl + "/json?location=" + latitude + "%2C" + longitude + "&" + RADIUS + "&type=" + type + "&key=" + apiKey) 44 | .method("GET", null) 45 | .build(); 46 | 47 | String responseBody; 48 | try (Response response = client.newCall(request).execute()) { 49 | responseBody = Objects.requireNonNull(response.body()).string(); 50 | } 51 | return objectMapper.readValue(responseBody, CustomPlacesResponse.class); 52 | } 53 | 54 | 55 | private static List removeDuplicateLocations(List places) { 56 | Map groupedPlaces = new HashMap<>(); 57 | 58 | places.stream().filter(place -> place.geometry() != null).forEach(place -> { 59 | String location = getLocationString(place.geometry().location()); 60 | groupedPlaces.putIfAbsent(location, place); 61 | }); 62 | 63 | return new ArrayList<>(groupedPlaces.values()); 64 | } 65 | 66 | private static String getLocationString(LatLng location) { 67 | return location.lat + "," + location.lng; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/service/PlayerService.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.service; 2 | 3 | import click.snekhome.backend.exception.NoSuchPlayerException; 4 | import click.snekhome.backend.model.Coordinates; 5 | import click.snekhome.backend.model.Player; 6 | import click.snekhome.backend.repo.PlayerRepo; 7 | import click.snekhome.backend.security.UserData; 8 | import click.snekhome.backend.util.IdService; 9 | import click.snekhome.backend.util.ItemSize; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.time.Instant; 14 | import java.util.List; 15 | 16 | import static click.snekhome.backend.util.PlayerFunctions.buyAttackPoints; 17 | 18 | @Service 19 | public class PlayerService { 20 | private final PlayerRepo playerRepo; 21 | public PlayerService(PlayerRepo playerRepo) { 22 | this.playerRepo = playerRepo; 23 | } 24 | 25 | public void createPlayer(UserData userData) { 26 | IdService idService = new IdService(); 27 | long fiveMinutesAgo = Instant.now().getEpochSecond() - 300; 28 | Player player = new Player( 29 | idService.generateId(), 30 | userData.id(), 31 | userData.username(), 32 | new Coordinates(0, 0, Instant.now().getEpochSecond()), 33 | 1, 34 | 0, 35 | 100, 36 | 100, 37 | 100, 38 | 5, 39 | 10, 40 | 0, 41 | fiveMinutesAgo 42 | ); 43 | this.playerRepo.save(player); 44 | } 45 | 46 | public Player getPlayer(String name) { 47 | return this.playerRepo.findPlayerByName(name).orElseThrow(); 48 | } 49 | 50 | public String getPlayerNameById(String id) { 51 | Player player = this.playerRepo.findPlayerByid(id).orElseThrow(() -> new NoSuchPlayerException(id)); 52 | return player.name(); 53 | } 54 | 55 | public Player updateLocation(Coordinates coordinates) { 56 | String username = SecurityContextHolder 57 | .getContext() 58 | .getAuthentication() 59 | .getName(); 60 | Player player = this.getPlayer(username); 61 | Player updatedPlayer = new Player( 62 | player.id(), 63 | player.userId(), 64 | player.name(), 65 | coordinates, 66 | player.level(), 67 | player.experience(), 68 | player.experienceToNextLevel(), 69 | player.health(), 70 | player.maxHealth(), 71 | player.attack(), 72 | player.maxAttack(), 73 | player.credits(), 74 | player.lastScan() 75 | ); 76 | return this.playerRepo.save(updatedPlayer); 77 | } 78 | 79 | public void updatePlayer(String id, Player updatedPlayer) { 80 | Player player = this.playerRepo.findPlayerByid(id).orElseThrow(() -> new NoSuchPlayerException(id)); 81 | Player newPlayer = new Player( 82 | player.id(), 83 | player.userId(), 84 | player.name(), 85 | player.coordinates(), 86 | updatedPlayer.level(), 87 | updatedPlayer.experience(), 88 | updatedPlayer.experienceToNextLevel(), 89 | updatedPlayer.health(), 90 | updatedPlayer.maxHealth(), 91 | updatedPlayer.attack(), 92 | updatedPlayer.maxAttack(), 93 | updatedPlayer.credits(), 94 | updatedPlayer.lastScan() 95 | ); 96 | this.playerRepo.save(newPlayer); 97 | } 98 | 99 | public List getEnemies() { 100 | String username = SecurityContextHolder 101 | .getContext() 102 | .getAuthentication() 103 | .getName(); 104 | Player player = this.getPlayer(username); 105 | return this.playerRepo.findAllByNameIsNot(player.name()); 106 | } 107 | 108 | public Player buyItem(ItemSize itemSize) { 109 | String username = SecurityContextHolder 110 | .getContext() 111 | .getAuthentication() 112 | .getName(); 113 | Player player = this.getPlayer(username); 114 | Player updatedPlayer = buyAttackPoints(player, itemSize); 115 | return this.playerRepo.save(updatedPlayer); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/util/ActionType.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | public enum ActionType { 4 | HACK("HACK"), 5 | ABANDON("ABANDON"); 6 | 7 | final String action; 8 | 9 | ActionType(String actionType) { 10 | this.action = actionType; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/util/Calculation.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | import java.time.Instant; 4 | 5 | public class Calculation { 6 | private static final double EARTH_RADIUS_KM = 6371.0; 7 | 8 | private Calculation() { 9 | } 10 | 11 | public static double getDistance(double lat1, double lon1, double lat2, double lon2) { 12 | double dLat = Math.toRadians(lat2 - lat1); 13 | double dLon = Math.toRadians(lon2 - lon1); 14 | 15 | double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) 16 | + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) 17 | * Math.sin(dLon / 2) * Math.sin(dLon / 2); 18 | 19 | double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 20 | 21 | return EARTH_RADIUS_KM * c * 1000; 22 | } 23 | 24 | public static long getSecondsSince(long timestamp) { 25 | return Instant.now().getEpochSecond() - timestamp; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/util/IdService.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.UUID; 6 | 7 | @Component 8 | public class IdService { 9 | 10 | public String generateId() { 11 | return UUID.randomUUID().toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/click/snekhome/backend/util/ItemSize.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | public enum ItemSize { 4 | SMALL("SMALL"), 5 | MEDIUM("MEDIUM"), 6 | LARGE("LARGE"); 7 | 8 | final String size; 9 | 10 | ItemSize(String itemSize) { 11 | this.size = itemSize; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.compression.enabled=true 2 | server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,text/css,application/javascript,application/x-javascript,image/svg+xml 3 | spring.data.mongodb.uri=${MONGO_DB_URI} 4 | player-api.url=/api/player 5 | mapbox.access.token=${MAPBOX_ACCESS_TOKEN} 6 | google.api.key=${GOOGLE_API_KEY} 7 | google.api.url=https://maps.googleapis.com/maps/api/place/nearbysearch 8 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/BackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | @SpringBootTest 9 | class BackendApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | assertTrue(true); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/controller/MapboxControllerTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.mock.mockito.MockBean; 8 | import org.springframework.http.*; 9 | import org.springframework.security.test.context.support.WithMockUser; 10 | import org.springframework.test.annotation.DirtiesContext; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 13 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 14 | import org.springframework.util.StreamUtils; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | import java.io.InputStream; 18 | 19 | import static org.mockito.Mockito.*; 20 | 21 | @SpringBootTest 22 | @AutoConfigureMockMvc 23 | class MapboxControllerTest { 24 | 25 | @MockBean 26 | private RestTemplate restTemplate; 27 | 28 | @Autowired 29 | private MockMvc mockMvc; 30 | 31 | @Test 32 | @DirtiesContext 33 | @WithMockUser(username = "test") 34 | void expectImageWithResponseEntity() throws Exception { 35 | InputStream imageInputStream = getClass().getResourceAsStream("/static/response.png"); 36 | byte[] imageBytes = StreamUtils.copyToByteArray(imageInputStream); 37 | 38 | HttpHeaders headers = new HttpHeaders(); 39 | headers.setContentType(MediaType.IMAGE_PNG); 40 | 41 | ResponseEntity mockResponseEntity = new ResponseEntity<>(imageBytes, headers, HttpStatus.OK); 42 | 43 | when(restTemplate.getForEntity( 44 | anyString(), 45 | eq(byte[].class), any(HttpEntity.class))) 46 | .thenReturn(mockResponseEntity); 47 | 48 | mockMvc.perform(MockMvcRequestBuilders.get("/api/map/5/0/0")) 49 | .andExpect(MockMvcResultMatchers.status().isOk()) 50 | .andExpect(MockMvcResultMatchers.content().bytes(imageBytes)) 51 | .andExpect(MockMvcResultMatchers.header().string(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/controller/WebSocketControllerUnitTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.InjectMocks; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | import org.springframework.web.socket.TextMessage; 9 | import org.springframework.web.socket.WebSocketSession; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.mockito.Mockito.*; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | class WebSocketControllerUnitTest { 17 | 18 | @InjectMocks 19 | private WebSocketController webSocketController; 20 | 21 | @Mock 22 | private WebSocketSession mockSession1; 23 | 24 | @Mock 25 | private WebSocketSession mockSession2; 26 | 27 | @Test 28 | void testAfterConnectionEstablished() throws Exception { 29 | when(mockSession1.getPrincipal()).thenReturn(() -> "username"); 30 | 31 | webSocketController.afterConnectionEstablished(mockSession1); 32 | 33 | verify(mockSession1).getPrincipal(); 34 | verify(mockSession1, times(2)).sendMessage(any(TextMessage.class)); 35 | } 36 | 37 | @Test 38 | void testHandleTextMessage() throws Exception { 39 | when(mockSession1.getPrincipal()).thenReturn(() -> "username"); 40 | 41 | String payload = "Hello!"; 42 | TextMessage textMessage = new TextMessage(payload); 43 | webSocketController.activeSessions.add(mockSession1); 44 | webSocketController.handleTextMessage(mockSession1, textMessage); 45 | 46 | verify(mockSession1).getPrincipal(); 47 | verify(mockSession1).sendMessage(any(TextMessage.class)); 48 | } 49 | 50 | @Test 51 | void testAfterConnectionClosed() throws Exception { 52 | when(mockSession1.getPrincipal()).thenReturn(() -> "username"); 53 | 54 | webSocketController.activeSessions.add(mockSession1); 55 | webSocketController.activeSessions.add(mockSession2); 56 | 57 | webSocketController.afterConnectionClosed(mockSession1, null); 58 | 59 | verify(mockSession1).getPrincipal(); 60 | verify(mockSession2, times(2)).sendMessage(any(TextMessage.class)); 61 | } 62 | 63 | @Test 64 | void testBroadcastMessage() throws IOException { 65 | webSocketController.activeSessions.add(mockSession1); 66 | 67 | webSocketController.broadcastMessage("Message"); 68 | 69 | verify(mockSession1).sendMessage(any(TextMessage.class)); 70 | } 71 | 72 | @Test 73 | void testWelcomeUser() throws IOException { 74 | webSocketController.welcomeUser(mockSession1); 75 | 76 | verify(mockSession1).sendMessage(any(TextMessage.class)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/security/MongoUserDetailServiceTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | import java.util.Optional; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.mockito.Mockito.*; 10 | 11 | class MongoUserDetailServiceTest { 12 | 13 | private final MongoUserRepository userRepository = mock(MongoUserRepository.class); 14 | private final MongoUserDetailService userDetailService = new MongoUserDetailService(userRepository); 15 | 16 | @Test 17 | void expectLoadedUserByUsername() { 18 | //given 19 | MongoUser expected = new MongoUser("abc", "playerunknown", "player@example.com", "password", Role.PLAYER); 20 | String username = "playerunknown"; 21 | //when 22 | when(userRepository.findByUsername(username)).thenReturn(Optional.of(expected)); 23 | UserDetails actual = userDetailService.loadUserByUsername("playerunknown"); 24 | //then 25 | assertEquals(expected.username(), actual.getUsername()); 26 | verify(userRepository).findByUsername(username); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/security/MongoUserServiceTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.security; 2 | 3 | import click.snekhome.backend.exception.UsernameAlreadyExistsException; 4 | import click.snekhome.backend.service.PlayerService; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | import static org.mockito.Mockito.*; 10 | 11 | class MongoUserServiceTest { 12 | private final MongoUserRepository mongoUserRepository = mock(MongoUserRepository.class); 13 | private final PlayerService playerService = mock(PlayerService.class); 14 | private final PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); 15 | private final MongoUserService mongoUserService = new MongoUserService(mongoUserRepository, playerService); 16 | 17 | @Test 18 | void expectUserData_whenUsernameIsGiven() { 19 | //given 20 | MongoUser expected = new MongoUser("abc", "playerunknown", "player@example.com", "password", Role.PLAYER); 21 | String username = "playerunknown"; 22 | //when 23 | when(mongoUserRepository.findByUsername(username)).thenReturn(java.util.Optional.of(expected)); 24 | UserData actual = mongoUserService.getUserDataByUsername(username); 25 | //then 26 | assertEquals(expected.username(), actual.username()); 27 | assertEquals(expected.id(), actual.id()); 28 | verify(mongoUserRepository).findByUsername(username); 29 | } 30 | 31 | @Test 32 | void expectUser_whenUsernameIsGiven() { 33 | //given 34 | MongoUser expected = new MongoUser("abc", "playerunknown", "player@example.com", "password", Role.PLAYER); 35 | String username = "playerunknown"; 36 | //when 37 | when(mongoUserRepository.findByUsername(username)).thenReturn(java.util.Optional.of(expected)); 38 | MongoUser actual = mongoUserService.getUserByUsername(username); 39 | //then 40 | assertEquals(expected, actual); 41 | verify(mongoUserRepository).findByUsername(username); 42 | } 43 | 44 | @Test 45 | void expectUsernameAlreadyExistsException_whenRegisteringWithExistingUsername() { 46 | //given 47 | String username = "playerunknown"; 48 | UserWithoutId user = new UserWithoutId("playerunknown", "player@example.com", "password"); 49 | UsernameAlreadyExistsException exception = new UsernameAlreadyExistsException("Username already exists"); 50 | //when 51 | when(mongoUserRepository.findByUsername(username)).thenThrow(exception); 52 | //then 53 | assertThrows(RuntimeException.class, () -> mongoUserService.registerUser(user)); 54 | assertInstanceOf(RuntimeException.class, exception); 55 | } 56 | 57 | @Test 58 | void expectPlayerCreation_whenRegisteringNewUser() { 59 | //given 60 | String username = "playerunknown"; 61 | UserWithoutId user = new UserWithoutId("playerunknown", "player@example.com", "password"); 62 | //when 63 | when(passwordEncoder.encode(user.password())).thenReturn("password"); 64 | when(mongoUserRepository.findByUsername(username)).thenReturn(java.util.Optional.empty()); 65 | mongoUserService.registerUser(user); 66 | //then 67 | verify(playerService).createPlayer(any()); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/service/PlayerServiceTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.service; 2 | 3 | import click.snekhome.backend.exception.NoSuchPlayerException; 4 | import click.snekhome.backend.model.Player; 5 | import click.snekhome.backend.repo.PlayerRepo; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Optional; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.when; 14 | 15 | class PlayerServiceTest { 16 | 17 | private final PlayerRepo playerRepo = mock(PlayerRepo.class); 18 | private final PlayerService playerService = new PlayerService(playerRepo); 19 | @Test 20 | void expectPlayerNameWhenIdIsGiven() { 21 | //given 22 | Player player = new Player("abc", "123", "playerunknown", null, 1, 0, 100, 100, 100, 5, 10, 0, 0); 23 | String id = "abc"; 24 | String expected = "playerunknown"; 25 | //when 26 | when(playerRepo.findPlayerByid(id)).thenReturn(Optional.of(player)); 27 | String actual = playerService.getPlayerNameById(id); 28 | //then 29 | assertEquals(expected, actual); 30 | } 31 | 32 | @Test 33 | void expectNoSuchPlayerExceptionWhenIdIsGiven() { 34 | //given 35 | String id = "abc"; 36 | //when 37 | when(playerRepo.findPlayerByid(id)).thenReturn(Optional.empty()); 38 | //then 39 | assertThrows(NoSuchPlayerException.class, () -> playerService.getPlayerNameById(id)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/util/IdServiceTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | class IdServiceTest { 8 | 9 | @Test 10 | void expectGeneratedStringOfLengthGreaterZero() { 11 | //given 12 | IdService idService = new IdService(); 13 | //when 14 | String actual = idService.generateId(); 15 | //then 16 | assertTrue(actual.length() > 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/test/java/click/snekhome/backend/util/NodeFunctionsTest.java: -------------------------------------------------------------------------------- 1 | package click.snekhome.backend.util; 2 | 3 | import click.snekhome.backend.model.Coordinates; 4 | import click.snekhome.backend.model.Node; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | class NodeFunctionsTest { 11 | 12 | @Test 13 | void testTakeDamageWithPositiveDamage() { 14 | Node initialNode = new Node("1", "player1", "Node1", 10, 100, new Coordinates(0, 0, 0), 0, 0); 15 | int damage = 30; 16 | int expectedNewHealth = initialNode.health() - (damage - damage * initialNode.level() / 100); 17 | if (expectedNewHealth < 0) { 18 | expectedNewHealth = 0; 19 | } 20 | Node newNode = NodeFunctions.takeDamage(initialNode, damage); 21 | assertEquals(expectedNewHealth, newNode.health()); 22 | } 23 | 24 | @Test 25 | void testTakeDamageWithNegativeDamage() { 26 | Node initialNode = new Node("1", "player1", "Node1", 10, 100, new Coordinates(0, 0, 0), 0, 0); 27 | int damage = -20; 28 | int expectedNewHealth = initialNode.health() - (damage - damage * initialNode.level() / 100); 29 | if (expectedNewHealth < 0) { 30 | expectedNewHealth = 0; 31 | } 32 | Node newNode = NodeFunctions.takeDamage(initialNode, damage); 33 | assertEquals(expectedNewHealth, newNode.health()); 34 | } 35 | 36 | @Test 37 | void testTakeDamageWithZeroDamage() { 38 | Node initialNode = new Node("1", "player1", "Node1", 10, 100, new Coordinates(0, 0, 0), 0, 0); 39 | Node newNode = NodeFunctions.takeDamage(initialNode, 0); 40 | assertEquals(initialNode.health(), newNode.health()); 41 | } 42 | 43 | @Test 44 | void testHealthCannotBeNegative() { 45 | Node initialNode = new Node("1", "player1", "Node1", 10, 100, new Coordinates(0, 0, 0), 0, 0); 46 | int damage = 200; 47 | Node newNode = NodeFunctions.takeDamage(initialNode, damage); 48 | assertEquals(0, newNode.health()); 49 | } 50 | 51 | @Test 52 | void testRandomLevelBasedOnNodeName() { 53 | String trading = "Trading interface"; 54 | String server = "Server farm"; 55 | String database = "Database access"; 56 | String cctv = "CCTV control"; 57 | String other = "Other"; 58 | int tradingLevel = NodeFunctions.calculateLevel(trading); 59 | int serverLevel = NodeFunctions.calculateLevel(server); 60 | int databaseLevel = NodeFunctions.calculateLevel(database); 61 | int cctvLevel = NodeFunctions.calculateLevel(cctv); 62 | int otherLevel = NodeFunctions.calculateLevel(other); 63 | assertTrue(tradingLevel >= 1 && tradingLevel <= 5); 64 | assertTrue(serverLevel >= 15 && serverLevel <= 30); 65 | assertTrue(databaseLevel >= 5 && databaseLevel <= 10); 66 | assertTrue(cctvLevel >= 1 && cctvLevel <= 10); 67 | assertTrue(otherLevel >= 1 && otherLevel <= 3); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | de.flapdoodle.mongodb.embedded.version=6.0.1 2 | player-api.url=/api/player 3 | mapbox.access.token=ey1234567890 4 | google.api.key=AI1234567890 5 | google.api.url=testUrl 6 | -------------------------------------------------------------------------------- /backend/src/test/resources/static/existing-resource.txt: -------------------------------------------------------------------------------- 1 | This is a test -------------------------------------------------------------------------------- /backend/src/test/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/src/test/resources/static/response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/backend/src/test/resources/static/response.png -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 | 26 | 27 | Netwalker 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "dev-host": "vite --host", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.11.1", 15 | "@emotion/styled": "^11.11.0", 16 | "@mui/material": "^5.14.2", 17 | "@types/leaflet.markercluster": "^1.5.1", 18 | "axios": "^1.4.0", 19 | "geolib": "^3.3.4", 20 | "leaflet": "^1.9.4", 21 | "leaflet.markercluster": "^1.5.3", 22 | "nanoid": "^4.0.2", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-leaflet": "^4.2.1", 26 | "react-router-dom": "^6.14.2", 27 | "react-toastify": "^9.1.3", 28 | "use-sound": "^4.0.1", 29 | "zustand": "^4.3.9" 30 | }, 31 | "devDependencies": { 32 | "@types/leaflet": "^1.9.3", 33 | "@types/react": "^18.2.15", 34 | "@types/react-dom": "^18.2.7", 35 | "@typescript-eslint/eslint-plugin": "^6.0.0", 36 | "@typescript-eslint/parser": "^6.0.0", 37 | "@vitejs/plugin-react": "^4.0.3", 38 | "eslint": "^8.45.0", 39 | "eslint-plugin-react-hooks": "^4.6.0", 40 | "eslint-plugin-react-refresh": "^0.4.3", 41 | "typescript": "^5.0.2", 42 | "vite": "^4.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/public/netrunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/public/netrunner.png -------------------------------------------------------------------------------- /frontend/src/GlobalStyle.tsx: -------------------------------------------------------------------------------- 1 | import {css, Global} from '@emotion/react'; 2 | import NerdFont from './assets/fonts/3270/3270NerdFont-Regular.ttf'; 3 | import Cyberpunk from './assets/fonts/Cyberpunk/Cyberpunk.ttf'; 4 | 5 | export default function GlobalStyle() { 6 | return ( 7 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/3270/3270NerdFont-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/fonts/3270/3270NerdFont-Regular.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Cyberpunk/Cyberpunk.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/fonts/Cyberpunk/Cyberpunk.ttf -------------------------------------------------------------------------------- /frontend/src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/images/avatar.png -------------------------------------------------------------------------------- /frontend/src/assets/images/defaultAvatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/images/defaultAvatar.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/defaultAvatar_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/images/defaultAvatar_background.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/intelligence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/images/intelligence.png -------------------------------------------------------------------------------- /frontend/src/assets/images/intelligence_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/images/intelligence_background.png -------------------------------------------------------------------------------- /frontend/src/assets/sounds/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/click.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/electric_machine.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/electric_machine.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/error.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/key_press.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/key_press.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/loading_os.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/loading_os.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/login_success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/login_success.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/switch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/switch.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/upgrade.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/upgrade.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/zoom_in.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/zoom_in.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sounds/zoom_out.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshydev/netRunner/c3d2ecc7357c9207fad32fe986f5b887da41f7ce/frontend/src/assets/sounds/zoom_out.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/svg/cctv_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/cctv_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/cctv_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/character_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/character_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/cpu_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/cpu_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/cpu_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/databanse_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/database_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/database_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/padlock_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/padlock_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/position.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/radio_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/radio_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/radio_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/talk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/trading_enemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/trading_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/trading_player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/components/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import {ActionType} from "../models.ts"; 2 | import {Button} from "@mui/material"; 3 | import styled from "@emotion/styled"; 4 | import React from "react"; 5 | 6 | type props = { 7 | action: ActionType 8 | onAction: (action: ActionType) => void 9 | inactive: boolean 10 | children?: React.ReactNode 11 | } 12 | 13 | export default function ActionButton({action, onAction, inactive, children}: props) { 14 | 15 | return onAction(action)} actiontype={action}>{children} 16 | } 17 | 18 | const StyledButton = styled(Button)<{actiontype: ActionType}>` 19 | width: 4rem; 20 | height: 4rem; 21 | scale: 0.6; 22 | font-family: inherit; 23 | background: var(--color-black); 24 | color: ${({actiontype}) => actiontype.toString() === "ABANDON" ? "var(--color-secondary)" : "var(--color-primary)"}; 25 | font-size: 4rem; 26 | border-radius: 12px; 27 | border: 4px solid ${({actiontype}) => actiontype.toString() === "ABANDON" ? "var(--color-secondary)" : "var(--color-primary)"}; 28 | transition: all 0.2s ease-in-out; 29 | 30 | &:focus { 31 | background: var(--color-black); 32 | } 33 | 34 | &:active { 35 | scale: 0.5; 36 | } 37 | 38 | &:disabled { 39 | color: var(--color-grey); 40 | border-color: var(--color-grey); 41 | } 42 | 43 | &:hover { 44 | background: var(--color-black); 45 | } 46 | `; -------------------------------------------------------------------------------- /frontend/src/components/AddButton.tsx: -------------------------------------------------------------------------------- 1 | import {useNavigate} from "react-router-dom"; 2 | import styled from "@emotion/styled"; 3 | import {Button, Tooltip} from "@mui/material"; 4 | import {useClickSound} from "../utils/sound.ts"; 5 | 6 | export default function AddButton() { 7 | const navigate = useNavigate(); 8 | 9 | const playClick = useClickSound(); 10 | 11 | return ( 12 | Add Node 14 | } placement={"left"}> 15 | { 16 | playClick(); 17 | navigate("/add") 18 | }}>+ 19 | 20 | ) 21 | } 22 | 23 | const StyledBadge = styled.div` 24 | color: var(--color-primary); 25 | font-size: 1.5rem; 26 | `; 27 | 28 | const StyledAddButton = styled(Button)` 29 | width: 4rem; 30 | height: 4rem; 31 | border-radius: 50%; 32 | background: var(--color-semiblack); 33 | border: 3px solid var(--color-primary); 34 | font-family: inherit; 35 | font-size: 2rem; 36 | color: var(--color-primary); 37 | position: fixed; 38 | bottom: 3rem; 39 | right: 2rem; 40 | filter: drop-shadow(0 0 0.75rem var(--color-black)); 41 | z-index: 5; 42 | 43 | &:hover { 44 | background: var(--color-black); 45 | } 46 | 47 | &:active { 48 | background: inherit; 49 | scale: 0.9; 50 | filter: drop-shadow(0 0 0.5rem var(--color-primary)); 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /frontend/src/components/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import {Message} from "../models.ts"; 2 | import styled from "@emotion/styled"; 3 | import {Card} from "@mui/material"; 4 | import {useStore} from "../hooks/useStore.ts"; 5 | 6 | type Props = { 7 | message: Message; 8 | } 9 | 10 | export default function ChatMessage({message}: Props) { 11 | const user = useStore(state => state.user); 12 | const isUser = message.username === user; 13 | const isAdmin = message.username === "Netwalker"; 14 | const usernameColor = (isUser: boolean, isAdmin: boolean) => { 15 | if (isUser) return "var(--color-secondary)"; 16 | if (isAdmin) return "var(--color-special)"; 17 | return "var(--color-primary)"; 18 | } 19 | 20 | return 21 | {message.username} 22 |

{message.message}

23 | {new Date(message.timestamp * 1000).toLocaleTimeString("en-US", { 24 | hour: "2-digit", 25 | minute: "2-digit", 26 | })} 27 |
28 | } 29 | 30 | const StyledMessage = styled(Card)<{ isuser: string, isadmin: string }>` 31 | width: 90%; 32 | height: auto; 33 | background: ${({isadmin}) => isadmin === "true" ? "var(--color-primary-dark)" : "var(--color-semiblack)"}; 34 | border-radius: 0.5rem; 35 | padding: 0.5rem; 36 | margin: 0.5rem 0; 37 | color: var(--color-primary); 38 | display: flex; 39 | flex-direction: column; 40 | ${props => props.isuser === "true" ? "margin-left: auto;" : ""} 41 | `; 42 | 43 | const StyledUsername = styled.h6<{color: string}>` 44 | color: ${({color}) => color}; 45 | `; 46 | 47 | const StyledTimestamp = styled.small` 48 | align-self: flex-end; 49 | font-size: 0.75rem; 50 | `; 51 | -------------------------------------------------------------------------------- /frontend/src/components/ChatPage.tsx: -------------------------------------------------------------------------------- 1 | import {Card} from "@mui/material"; 2 | import styled from "@emotion/styled"; 3 | import {StyledInput} from "./styled/StyledInput.ts"; 4 | import {StyledFormButton} from "./styled/StyledFormButton.ts"; 5 | import React, {useEffect, useRef, useState} from "react"; 6 | import ChatMessage from "./ChatMessage.tsx"; 7 | import {nanoid} from "nanoid"; 8 | import {useStore} from "../hooks/useStore.ts"; 9 | 10 | export default function ChatPage() { 11 | const [text, setText] = useState("") 12 | const messages = useStore(state => state.messages); 13 | const sendMessage = useStore(state => state.sendMessage); 14 | const messageListRef = useRef(null); 15 | 16 | useEffect(() => { 17 | if (messageListRef.current) { 18 | messageListRef.current.scrollTop = messageListRef.current.scrollHeight; 19 | } 20 | }, [messages]); 21 | 22 | function handleSubmit(e: React.FormEvent) { 23 | e.preventDefault(); 24 | sendMessage(text); 25 | setText("") 26 | e.currentTarget.reset() 27 | e.currentTarget.focus() 28 | } 29 | 30 | return 31 | 32 | {messages.length > 0 ? messages.map((message) => { 33 | return 34 | }) :

...

} 35 |
36 | 37 | setText(event.target.value)} 41 | maxLength={500} /> 42 | Send 43 | 44 |
45 | } 46 | 47 | const StyledCard = styled(Card)` 48 | margin: 0.5rem 0; 49 | width: 95%; 50 | height: 70vh; 51 | background: var(--color-semiblack); 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | padding: 1rem; 56 | gap: 1rem; 57 | `; 58 | 59 | const StyledChatWindow = styled.ul` 60 | width: 100%; 61 | height: 90%; 62 | background: var(--color-black); 63 | border-radius: 0.5rem; 64 | overflow-y: scroll; 65 | padding: 0.5rem; 66 | box-shadow: 0 0 0.5rem var(--color-primary); 67 | 68 | &::-webkit-scrollbar { 69 | width: 0.5rem; 70 | } 71 | 72 | &::-webkit-scrollbar-track { 73 | background: transparent; 74 | } 75 | 76 | &::-webkit-scrollbar-thumb { 77 | background: var(--color-secondary); 78 | border-radius: 0.5rem; 79 | } 80 | 81 | &::-webkit-scrollbar-thumb:hover { 82 | background: var(--color-secondary); 83 | border-radius: 0.5rem; 84 | } 85 | `; 86 | 87 | const StyledInputForm = styled.form` 88 | width: 100%; 89 | height: 10%; 90 | display: flex; 91 | flex-direction: row; 92 | align-items: center; 93 | justify-content: space-between; 94 | gap: 0.5rem; 95 | `; 96 | 97 | const StyledInputField = styled(StyledInput)` 98 | width: 90%; 99 | height: 100%; 100 | background: var(--color-semiblack); 101 | text-align: left; 102 | border-radius: 0.5rem; 103 | color: var(--color-primary); 104 | outline: 2px solid var(--color-grey); 105 | font-size: 1rem; 106 | padding: 0.5rem; 107 | text-shadow: none; 108 | 109 | &:focus { 110 | outline: 2px solid var(--color-primary); 111 | background: var(--color-black); 112 | } 113 | 114 | &::placeholder { 115 | color: var(--color-white); 116 | } 117 | 118 | &:hover { 119 | background: var(--color-black); 120 | } 121 | `; 122 | 123 | const StyledSendButton = styled(StyledFormButton)` 124 | width: 5%; 125 | height: 100%; 126 | filter: none; 127 | 128 | &:hover { 129 | background: var(--color-semiblack); 130 | } 131 | 132 | &:active { 133 | filter: drop-shadow(0 0 0.5rem var(--color-primary)); 134 | background: var(--color-black); 135 | } 136 | 137 | &:disabled { 138 | filter: none; 139 | color: var(--color-grey); 140 | border-color: var(--color-grey); 141 | } 142 | `; 143 | -------------------------------------------------------------------------------- /frontend/src/components/ChatViewButton.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "@mui/material"; 2 | import {useLocation, useNavigate} from "react-router-dom"; 3 | import styled from "@emotion/styled"; 4 | import {useClickSound} from "../utils/sound.ts"; 5 | import ChatIcon from "./icons/ChatIcon.tsx"; 6 | import {useStore} from "../hooks/useStore.ts"; 7 | import {useEffect} from "react"; 8 | 9 | export default function ChatViewButton() { 10 | const unreadMessages = useStore(state => state.unreadMessages) 11 | const resetUnreadMessages = useStore(state => state.resetUnreadMessages) 12 | const navigate = useNavigate(); 13 | const location = useLocation(); 14 | const playClick = useClickSound() 15 | 16 | const path = location.pathname.split("/")[1] 17 | 18 | useEffect(() => { 19 | if (path === "chat") { 20 | resetUnreadMessages() 21 | } 22 | }, [path, resetUnreadMessages, unreadMessages]); 23 | 24 | return { 25 | playClick() 26 | navigate("/chat") 27 | }}> 28 | 29 | {unreadMessages > 0 && {unreadMessages}} 30 | 31 | } 32 | 33 | const StyledViewButton = styled(Button)<{ispage: string}>` 34 | width: 4rem; 35 | height: 4rem; 36 | border-radius: 8px; 37 | background: var(--color-black); 38 | ${props => props.ispage === "true" ? "border: 3px solid var(--color-primary);" : ""} 39 | font-family: inherit; 40 | color: var(--color-primary); 41 | scale: 0.65; 42 | position: relative; 43 | 44 | &:hover { 45 | background: var(--color-black); 46 | } 47 | 48 | &:active { 49 | background: inherit; 50 | scale: 0.6; 51 | filter: drop-shadow(0 0 0.5rem var(--color-primary)); 52 | } 53 | 54 | svg { 55 | width: 2rem; 56 | height: 2rem; 57 | } 58 | `; 59 | 60 | const StyledMessageBadge = styled.div` 61 | position: absolute; 62 | top: -0.5rem; 63 | right: -0.5rem; 64 | width: 2rem; 65 | height: 2rem; 66 | border-radius: 50%; 67 | background: var(--color-secondary); 68 | color: white; 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | font-size: 1.5rem; 73 | font-weight: bold; 74 | filter: drop-shadow(0 0 0.5rem var(--color-black)); 75 | 76 | `; 77 | -------------------------------------------------------------------------------- /frontend/src/components/CooldownCounter.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | import styled from "@emotion/styled"; 3 | 4 | type Props = { 5 | lastActionTimestamp: number; 6 | duration: number; 7 | label: "Update" | "Attack"; 8 | }; 9 | 10 | export default function CooldownCounter({ lastActionTimestamp, duration, label }: Props) { 11 | const [remainingCooldown, setRemainingCooldown] = useState(0); 12 | 13 | useEffect(() => { 14 | const now = Math.floor(Date.now() / 1000); 15 | const remaining = Math.max(0, lastActionTimestamp + duration - now); 16 | setRemainingCooldown(remaining); 17 | 18 | if (remaining > 0) { 19 | const cooldownInterval = setInterval(() => { 20 | const updatedRemaining = Math.max(0, lastActionTimestamp + duration - Math.floor(Date.now() / 1000)); 21 | setRemainingCooldown(updatedRemaining); 22 | if (updatedRemaining === 0) { 23 | clearInterval(cooldownInterval); 24 | } 25 | }, 1000); 26 | 27 | return () => clearInterval(cooldownInterval); 28 | } 29 | }, [lastActionTimestamp]); 30 | 31 | function formatTime(timeInSeconds: number) { 32 | const minutes = Math.floor(timeInSeconds / 60); 33 | const seconds = timeInSeconds % 60; 34 | return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; 35 | } 36 | 37 | return 38 | {formatTime(remainingCooldown)} 39 | 40 | } 41 | 42 | const StyledCountdownText = styled.p<{type: string}>` 43 | font-size: 1rem; 44 | color: ${props => props.type === "Update" ? "var(--color-primary)" : "var(--color-secondary)"}; 45 | border: 2px solid ${props => props.type === "Update" ? "var(--color-primary)" : "var(--color-secondary)"}; 46 | border-radius: 5px; 47 | text-align: center; 48 | padding: 0 0.25rem; 49 | background: var(--color-black); 50 | `; 51 | -------------------------------------------------------------------------------- /frontend/src/components/GpsButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import {Button, Tooltip} from "@mui/material"; 3 | import {useSwitchSound} from "../utils/sound.ts"; 4 | import {useStore} from "../hooks/useStore.ts"; 5 | import PositionIcon from "./icons/PositionIcon.tsx"; 6 | 7 | export default function GpsButton() { 8 | const gps = useStore(state => state.gps); 9 | const setGps = useStore(state => state.setGps); 10 | 11 | const playSwitchSound = useSwitchSound(); 12 | 13 | return ( 14 | Toggle GPS 16 | } placement={"right"}> 17 | { 18 | playSwitchSound(); 19 | setGps(!gps); 20 | }} gps={`${gps}`} 21 | > 22 | 23 | ) 24 | } 25 | 26 | const StyledBadge = styled.div` 27 | color: var(--color-primary); 28 | font-size: 1.5rem; 29 | `; 30 | 31 | const StyledAddButton = styled(Button)<{gps: string }>` 32 | width: 4rem; 33 | height: 4rem; 34 | border-radius: 50%; 35 | background: var(--color-black); 36 | border: 3px solid ${props => props.gps === "true" ? "var(--color-primary)" : "var(--color-secondary)"}; 37 | font-family: inherit; 38 | font-size: 2rem; 39 | color: ${props => props.gps === "true" ? "var(--color-primary)" : "var(--color-secondary)"}; 40 | position: fixed; 41 | bottom: 6.5rem; 42 | left: 0.5rem; 43 | filter: drop-shadow(0 0 0.75rem var(--color-black)); 44 | z-index: 5; 45 | transform: scale(0.6); 46 | 47 | &:hover { 48 | background: var(--color-black); 49 | } 50 | 51 | &:active { 52 | background: var(--color-black); 53 | scale: 0.9; 54 | filter: drop-shadow(0 0 0.5rem ${props => props.gps === "true" ? "var(--color-primary)" : "var(--color-secondary)"}); 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /frontend/src/components/HealthBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | type Props = { 4 | health: number; 5 | } 6 | export default function HealthBar({health}: Props) { 7 | 8 | return ( 9 | 10 | 11 | {health}% 12 | 13 | ) 14 | } 15 | 16 | const StyledHealthBarContainer = styled.div` 17 | width: 7rem; 18 | height: 1.5rem; 19 | background: var(--color-black); 20 | border: 2px solid var(--color-black); 21 | border-radius: 8px; 22 | position: relative; 23 | `; 24 | 25 | const StyledHealthBar = styled.div<{ health: number }>` 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | width: ${({ health }) => health}%; 30 | height: 100%; 31 | background: ${({ health }) => health > 50 ? "var(--color-primary)" : "var(--color-secondary)"}; 32 | border-radius: 8px; 33 | filter: drop-shadow(0 0 2px ${({ health }) => health > 50 ? "var(--color-primary)" : "var(--color-secondary)"}); 34 | `; 35 | 36 | const StyledHealthText = styled.div<{health: number}>` 37 | position: absolute; 38 | top: 50%; 39 | left: 50%; 40 | transform: translate(-50%, -50%); 41 | color: ${({health}) => health > 50 ? "black" : "white"}; 42 | text-shadow: 0 0 1px ${({health}) => health > 50 ? "black" : "white"}; 43 | `; 44 | -------------------------------------------------------------------------------- /frontend/src/components/MiniActionButton.tsx: -------------------------------------------------------------------------------- 1 | import {ActionType} from "../models.ts"; 2 | import {Button} from "@mui/material"; 3 | import styled from "@emotion/styled"; 4 | import React from "react"; 5 | 6 | type props = { 7 | action: ActionType 8 | onAction: (action: ActionType) => void 9 | inactive: boolean 10 | children?: React.ReactNode 11 | } 12 | 13 | export default function MiniActionButton({action, onAction, inactive, children}: props) { 14 | 15 | return onAction(action)} actiontype={action}>{children} 16 | } 17 | 18 | const StyledButton = styled(Button)<{actiontype: ActionType}>` 19 | font-family: inherit; 20 | background: var(--color-black); 21 | color: ${({actiontype}) => actiontype.toString() === "ABANDON" ? "var(--color-secondary)" : "var(--color-primary)"}; 22 | border-radius: 5px; 23 | border: 2px solid ${({actiontype}) => actiontype.toString() === "ABANDON" ? "var(--color-secondary)" : "var(--color-primary)"}; 24 | transition: all 0.2s ease-in-out; 25 | min-width: 1rem; 26 | min-height: 1rem; 27 | width: 1.5rem; 28 | height: 1.5rem; 29 | 30 | svg { 31 | scale: 4 32 | } 33 | 34 | &:focus { 35 | background: var(--color-black); 36 | } 37 | 38 | &:active { 39 | scale: 0.5; 40 | } 41 | 42 | &:disabled { 43 | color: var(--color-grey); 44 | border-color: var(--color-grey); 45 | } 46 | 47 | &:hover { 48 | background: var(--color-black); 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /frontend/src/components/NodeFilter.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import {useStore} from "../hooks/useStore.ts"; 3 | import {Button} from "@mui/material"; 4 | import CharacterIcon from "./icons/CharacterIcon.tsx"; 5 | import NetworkIcon from "./icons/NetworkIcon.tsx"; 6 | import {useClickSound} from "../utils/sound.ts"; 7 | 8 | export default function NodeFilter() { 9 | const sortDirection = useStore(state => state.sortDirection); 10 | const toggleSortDirection = useStore(state => state.toggleSortDirection); 11 | const ownerNodesFilter = useStore(state => state.ownerNodesFilter); 12 | const toggleOwnerNodesFilter = useStore(state => state.toggleOwnerNodesFilter); 13 | const rangeFilter = useStore(state => state.rangeFilter); 14 | const toggleRangeFilter = useStore(state => state.toggleRangeFilter); 15 | 16 | const playClick = useClickSound() 17 | 18 | return 19 | { 20 | playClick() 21 | toggleRangeFilter() 22 | }}> 23 | 24 | 25 | { 26 | playClick() 27 | toggleOwnerNodesFilter() 28 | }}> 29 | 30 | 31 | { 32 | playClick() 33 | toggleSortDirection() 34 | }}>{sortDirection === "asc" ? "▲" : "▼"} 35 | 36 | } 37 | 38 | const StyledContainer = styled.div` 39 | display: flex; 40 | justify-content: flex-end; 41 | align-items: center; 42 | width: 100%; 43 | height: 3rem; 44 | position: sticky; 45 | top: 6.5rem; 46 | z-index: 6; 47 | `; 48 | 49 | const StyledSortToggle = styled(Button)<{direction: string}>` 50 | color: var(--color-${props => props.direction === "asc" ? "primary" : "secondary"}); 51 | background: var(--color-black); 52 | border: 2px solid var(--color-${props => props.direction === "asc" ? "primary" : "secondary"}); 53 | border-radius: 8px; 54 | width: 4rem; 55 | height: 4rem; 56 | transform: scale(0.5); 57 | font-size: 3rem; 58 | display: flex; 59 | justify-content: center; 60 | align-items: center; 61 | filter: drop-shadow(0 0 0.75rem var(--color-black)); 62 | transition: all 0.2s ease-in-out; 63 | 64 | &:hover { 65 | background: var(--color-black); 66 | } 67 | 68 | &:active { 69 | scale: 0.9; 70 | } 71 | `; 72 | 73 | const StyledFilterToggle = styled(StyledSortToggle)<{direction: string}>` 74 | color: var(--color-${props => props.direction === "true" ? "primary" : "grey"}); 75 | border: 2px solid var(--color-${props => props.direction === "true" ? "primary" : "grey"}); 76 | `; 77 | -------------------------------------------------------------------------------- /frontend/src/components/NodeList.tsx: -------------------------------------------------------------------------------- 1 | import NodeItem from "./NodeItem.tsx"; 2 | import {useStore} from "../hooks/useStore.ts"; 3 | import styled from "@emotion/styled"; 4 | import {useEffect, useState} from "react"; 5 | import {Node, Player} from "../models.ts"; 6 | import {getDistanceBetweenCoordinates} from "../utils/calculation.ts"; 7 | 8 | type Props = { 9 | player: Player | null; 10 | nodes: Node[]; 11 | } 12 | 13 | export default function NodeList({ player, nodes }: Props) { 14 | const [sortedNodes, setSortedNodes] = useState([]); 15 | const getNodes = useStore(state => state.getNodes); 16 | const user = useStore(state => state.user); 17 | const sortDirection = useStore(state => state.sortDirection); 18 | const sortNodesByDistance = useStore(state => state.sortNodesByDistance); 19 | const ownerNodesFilter = useStore(state => state.ownerNodesFilter); 20 | const filterNodesByOwner = useStore(state => state.filterNodesByOwner); 21 | const rangeFilter = useStore(state => state.rangeFilter); 22 | const filterNodesByRange = useStore(state => state.filterNodesByRange); 23 | 24 | useEffect(() => { 25 | if (user !== "" && user !== "anonymousUser") { 26 | if (nodes && player) { 27 | const filteredNodesbyOwner = filterNodesByOwner(player.id, nodes); 28 | const filteredNodesByRange = filterNodesByRange({latitude: player.coordinates.latitude, longitude: player.coordinates.longitude}, filteredNodesbyOwner, 250); 29 | setSortedNodes(sortNodesByDistance({latitude: player.coordinates.latitude, longitude: player.coordinates.longitude}, filteredNodesByRange)); 30 | } 31 | } 32 | }, [getNodes, nodes, player, user, sortDirection, ownerNodesFilter, rangeFilter, filterNodesByOwner, filterNodesByRange, sortNodesByDistance]); 33 | 34 | if (player) { 35 | return ( 36 | 37 | {sortedNodes.map(node => ( 38 | 51 | ))} 52 | 53 | ); 54 | } 55 | } 56 | 57 | const StyledList = styled.ul` 58 | list-style: none; 59 | width: 100%; 60 | padding: 0; 61 | display: flex; 62 | flex-direction: column; 63 | align-items: center; 64 | gap: 1rem; 65 | margin-bottom: 8rem; 66 | `; 67 | -------------------------------------------------------------------------------- /frontend/src/components/PlayerInfoBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import {Player} from "../models.ts"; 3 | import CpuIcon from "./icons/CpuIcon.tsx"; 4 | import {useNavigate} from "react-router-dom"; 5 | import {useClickSound} from "../utils/sound.ts"; 6 | 7 | type Props = { 8 | player: Player | null 9 | } 10 | export default function PlayerInfoBar({player}: Props) { 11 | const navigate = useNavigate() 12 | const playClick = useClickSound() 13 | 14 | function handleNavigate(path: string) { 15 | playClick() 16 | navigate(path) 17 | } 18 | 19 | return 20 | {player && 21 | handleNavigate(`/player/${player.name}`)}>{player.name} 22 | 23 | LVL {player.level} 24 | {player.health}HP 25 | handleNavigate("/store")}> 26 | {player.attack} 27 | 28 | 29 | {player.credits}$ 30 | 31 | } 32 | 33 | 34 | } 35 | 36 | const StyledContainer = styled.div` 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | justify-content: center; 41 | width: 95%; 42 | height: 3rem; 43 | gap: 0.5rem; 44 | position: sticky; 45 | top: 3.5rem; 46 | z-index: 6; 47 | background: linear-gradient(var(--color-black) 0%, var(--color-black) 90%, transparent); 48 | `; 49 | 50 | const StyledBar = styled.div<{ theme: string, bg: string }>` 51 | display: flex; 52 | justify-content: space-between; 53 | height: 75%; 54 | align-items: center; 55 | border: 2px solid var(--color-${props => props.theme}); 56 | border-radius: 8px; 57 | color: var(--color-${props => props.theme}); 58 | width: 100%; 59 | background: var(--color-${props => props.bg}); 60 | padding: 0 1rem 0 1rem; 61 | transition: all 0.5s ease-in-out; 62 | `; 63 | 64 | const StyledText = styled.p<{ color: string }>` 65 | color: var(--color-${props => props.color}); 66 | 67 | &:active { 68 | scale: 0.95; 69 | } 70 | `; 71 | 72 | const StyledInfoContainer = styled.div` 73 | display: flex; 74 | justify-content: space-evenly; 75 | align-items: center; 76 | gap: 1rem; 77 | `; 78 | 79 | const StyledDaemonsContainer = styled.div` 80 | height: 100%; 81 | display: flex; 82 | justify-content: space-around; 83 | align-items: center; 84 | 85 | &:active { 86 | scale: 0.95; 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /frontend/src/components/PlayerPopup.tsx: -------------------------------------------------------------------------------- 1 | import {Player} from "../models.ts"; 2 | import {Popup} from "react-leaflet"; 3 | import styled from "@emotion/styled"; 4 | import {Button} from "@mui/material"; 5 | import {useNavigate} from "react-router-dom"; 6 | 7 | type Props = { 8 | player: Player 9 | } 10 | 11 | export default function PlayerPopup({player}: Props) { 12 | const navigate = useNavigate() 13 | 14 | return 15 | navigate(`/player/${player.name}`)}>{player.name} 16 | 17 | } 18 | 19 | const StyledPlayerName = styled(Button)` 20 | background: var(--color-black); 21 | color: var(--color-secondary); 22 | border: 2px solid currentColor; 23 | border-radius: 5px; 24 | font-family: "3270", monospace; 25 | height: 100%; 26 | text-transform: unset; 27 | `; 28 | -------------------------------------------------------------------------------- /frontend/src/components/ProtectedRoutes.tsx: -------------------------------------------------------------------------------- 1 | import {Navigate, Outlet} from "react-router-dom"; 2 | 3 | type Props = { 4 | user?: string 5 | } 6 | 7 | export default function ProtectedRoutes({user}: Props) { 8 | 9 | if(user === undefined) return "loading..."; 10 | 11 | const isAuthenticated = user !== "anonymousUser" 12 | 13 | return <>{isAuthenticated ? : } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/RechargingButton.tsx: -------------------------------------------------------------------------------- 1 | import useCooldown from "../hooks/useCooldown.ts"; 2 | import styled from "@emotion/styled"; 3 | import {Button} from "@mui/material"; 4 | import ScanIcon from "./icons/ScanIcon.tsx"; 5 | import {Player} from "../models.ts"; 6 | import {useStore} from "../hooks/useStore.ts"; 7 | import {useLoadingOsSound, useErrorSound} from "../utils/sound.ts"; 8 | import {useEffect, useState} from "react"; 9 | import {keyframes} from "@emotion/react"; 10 | 11 | type Props = { 12 | player: Player; 13 | } 14 | export default function RechargingButton({player}: Props) { 15 | const [timestamp, setTimestamp] = useState(0); 16 | const [percentage, setPercentage] = useState(0); 17 | const {isOnCooldown, counter} = useCooldown(timestamp, 300); 18 | const scanNodes = useStore(state => state.scanNodes); 19 | const playLoading = useLoadingOsSound(); 20 | const playError = useErrorSound(); 21 | 22 | useEffect(() => { 23 | setTimestamp(player.lastScan) 24 | if (isOnCooldown) { 25 | setPercentage(Math.ceil(100 - counter / 300 * 100)) 26 | } 27 | }, [counter, isOnCooldown, scanNodes, player]); 28 | 29 | function handleScan() { 30 | setTimestamp(Date.now()); 31 | scanNodes(player.coordinates, playLoading, playError); 32 | } 33 | 34 | return 35 | 36 | {isOnCooldown && } 37 | 38 | 39 | 40 | } 41 | 42 | const blink = keyframes` 43 | 0% { 44 | filter: drop-shadow(0 0 0.25rem var(--color-black)); 45 | } 46 | 47 | 50% { 48 | filter: drop-shadow(0 0 0.25rem var(--color-secondary)); 49 | } 50 | 51 | 100% { 52 | filter: drop-shadow(0 0 0.25rem var(--color-black)); 53 | } 54 | `; 55 | 56 | const StyledButton = styled(Button)<{isupdating: string}>` 57 | position: fixed; 58 | bottom: 3rem; 59 | right: 2rem; 60 | background: var(--color-black); 61 | border: 3px solid ${({isupdating}) => isupdating === "false" ? "var(--color-primary)" : "var(--color-semiblack)"}; 62 | border-radius: 50%; 63 | width: 4rem; 64 | height: 4rem; 65 | z-index: 9; 66 | padding: 0; 67 | color: ${({isupdating}) => isupdating === "false" ? "var(--color-primary)" : "var(--color-semiblack)"}; 68 | filter: drop-shadow(0 0 0.25rem ${({isupdating}) => isupdating === "false" ? "var(--color-primary)" : "var(--color-black)"}); 69 | animation: ${({isupdating}) => isupdating === "true" ? blink : "none"} 1s infinite; 70 | 71 | &:hover { 72 | background: var(--color-black); 73 | } 74 | 75 | &:active { 76 | background: var(--color-black); 77 | scale: 0.9; 78 | filter: drop-shadow(0 0 1rem var(--color-primary)); 79 | } 80 | `; 81 | 82 | const StyledBackgroundContainer = styled.div` 83 | position: relative; 84 | width: 100%; 85 | height: 100%; 86 | clip-path: circle(50% at 50% 50%); 87 | 88 | svg { 89 | position: relative; 90 | top: 15%; 91 | width: 1.5rem; 92 | height: 2.5rem; 93 | } 94 | `; 95 | 96 | const StyledRechargingBackground = styled.div<{percentage: number}>` 97 | position: absolute; 98 | bottom: 0; 99 | left: 0; 100 | background: var(--color-primary); 101 | width: 100%; 102 | height: ${({percentage}) => percentage < 100 ? percentage : 0}%; 103 | opacity: 0.8; 104 | filter: drop-shadow(0 0 0.25rem var(--color-primary)); 105 | `; 106 | -------------------------------------------------------------------------------- /frontend/src/components/SettingsPage.tsx: -------------------------------------------------------------------------------- 1 | import {Card} from "@mui/material"; 2 | import styled from "@emotion/styled"; 3 | import VolumeBar from "./VolumeBar.tsx"; 4 | 5 | export default function SettingsPage() { 6 | return 7 | Sound 8 | 9 | 10 | } 11 | 12 | const StyledCard = styled(Card)` 13 | margin: 0.5rem 0; 14 | width: 95%; 15 | height: 70vh; 16 | background: var(--color-semiblack); 17 | display: flex; 18 | align-items: center; 19 | flex-direction: column; 20 | padding: 1rem; 21 | gap: 1rem; 22 | `; 23 | 24 | const StyledHeading = styled.h2` 25 | color: var(--color-primary); 26 | `; 27 | -------------------------------------------------------------------------------- /frontend/src/components/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import {StyledStatusBar} from "./styled/StyledStatusBar.ts"; 2 | import styled from "@emotion/styled"; 3 | import {useStore} from "../hooks/useStore.ts"; 4 | import React, {useEffect, useState} from "react"; 5 | import {useClickSound} from "../utils/sound.ts"; 6 | import {Menu, MenuItem} from "@mui/material"; 7 | import {useNavigate} from "react-router-dom"; 8 | 9 | type Props = { 10 | gps: boolean; 11 | } 12 | 13 | export default function StatusBar({gps}: Props) { 14 | const [anchorElUser, setAnchorElUser] = useState(null); 15 | const [playerList, setPlayerList] = useState([]); 16 | const activePlayers = useStore(state => state.activePlayers); 17 | const playClick = useClickSound(); 18 | const navigate = useNavigate(); 19 | 20 | useEffect(() => { 21 | setPlayerList(activePlayers) 22 | }, [activePlayers]); 23 | 24 | function handleOpenUserMenu(event: React.MouseEvent) { 25 | playClick() 26 | setAnchorElUser(event.currentTarget); 27 | } 28 | 29 | function handleCloseUserMenu() { 30 | playClick() 31 | setAnchorElUser(null); 32 | } 33 | 34 | function handleNavigate(path: string) { 35 | playClick() 36 | setAnchorElUser(null); 37 | navigate(path) 38 | } 39 | 40 | return ( 41 | 42 | 43 |

{gps ? "GPS enabled" : "GPS disabled"}

44 | {gps && } 45 |
46 | 47 | {playerList.length} Netwalker{playerList.length > 1 && "s"} online 48 | 49 | 64 | {playerList.length > 0 && playerList.map((player) => { 65 | return handleNavigate(`/player/${player}`)}>{player} 66 | })} 67 | 68 |
69 | ) 70 | } 71 | 72 | const StyledGpsContainer = styled.div<{gps: boolean}>` 73 | height: 100%; 74 | display: flex; 75 | justify-content: space-between; 76 | align-items: center; 77 | position: relative; 78 | 79 | p { 80 | font-size: 1rem; 81 | color: ${({gps}) => gps ? "var(--color-primary)" : "var(--color-secondary)"}; 82 | transition: color 0.5s ease-in-out; 83 | } 84 | `; 85 | 86 | const StyledRotatingTriangle = styled.div` 87 | width: 0; 88 | height: 0; 89 | border: 0.4rem solid transparent; 90 | border-top: 0; 91 | border-bottom: 0.65rem solid var(--color-primary); 92 | color: var(--color-primary); 93 | position: absolute; 94 | top: 50%; 95 | left: 6.5rem; 96 | transform: translate(-50%, -20%); 97 | animation: rotate 4s ease-in-out infinite; 98 | 99 | @keyframes rotate { 100 | 0% { 101 | transform: translate(-50%, -50%) rotate(0deg); 102 | } 103 | 50% { 104 | transform: translate(-50%, -50%) rotate(900deg); 105 | } 106 | 100% { 107 | transform: translate(-50%, -50%) rotate(0deg); 108 | } 109 | } 110 | `; 111 | 112 | const StyledMenu = styled(Menu)<{count: number}>` 113 | .MuiPaper-root { 114 | background: var(--color-semiblack); 115 | color: var(--color-primary); 116 | filter: drop-shadow(0 0 1rem var(--color-black)); 117 | } 118 | `; 119 | 120 | const StyledText = styled.p<{count: number}>` 121 | color: ${({count}) => count > 0 ? "var(--color-primary)" : "var(--color-secondary)"}; 122 | `; 123 | -------------------------------------------------------------------------------- /frontend/src/components/ViewChangeButton.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "@mui/material"; 2 | import {useNavigate} from "react-router-dom"; 3 | import NavigationIcon from "./icons/NavigationIcon.tsx"; 4 | import ListIcon from "./icons/ListIcon.tsx"; 5 | import styled from "@emotion/styled"; 6 | import {useSwitchSound} from "../utils/sound.ts"; 7 | 8 | type Props = { 9 | view: string; 10 | } 11 | 12 | export default function ViewChangeButton({view}: Props) { 13 | const navigate = useNavigate(); 14 | const playSwitch = useSwitchSound() 15 | 16 | return { 17 | playSwitch() 18 | navigate(view === "map" ? "/map" : "/") 19 | }}> 20 | {view === "map" ? : } 21 | 22 | } 23 | 24 | const StyledViewButton = styled(Button)` 25 | width: 4rem; 26 | height: 4rem; 27 | border-radius: 50%; 28 | background: var(--color-black); 29 | border: 3px solid var(--color-primary); 30 | font-family: inherit; 31 | color: var(--color-primary); 32 | position: fixed; 33 | bottom: 3rem; 34 | left: 2rem; 35 | filter: drop-shadow(0 0 0.75rem var(--color-black)); 36 | z-index: 9; 37 | 38 | &:hover { 39 | background: var(--color-black); 40 | } 41 | 42 | &:active { 43 | background: inherit; 44 | scale: 0.9; 45 | filter: drop-shadow(0 0 0.5rem var(--color-primary)); 46 | } 47 | 48 | svg { 49 | width: 2rem; 50 | height: 2rem; 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /frontend/src/components/VolumeBar.tsx: -------------------------------------------------------------------------------- 1 | import {useStore} from "../hooks/useStore.ts"; 2 | import styled from "@emotion/styled"; 3 | import {Button} from "@mui/material"; 4 | import SoundIcon from "./icons/SoundIcon.tsx"; 5 | import {useEffect, useState} from "react"; 6 | import {useClickSound} from "../utils/sound.ts"; 7 | 8 | export default function VolumeBar() { 9 | const [previousVolume, setPreviousVolume] = useState(0) 10 | const volume = useStore(state => state.volume) 11 | const setVolume = useStore(state => state.setVolume) 12 | const playClick = useClickSound() 13 | 14 | useEffect(() => { 15 | setPreviousVolume(volume) 16 | }, []); 17 | 18 | function handleVolumeToggle() { 19 | playClick() 20 | if (volume === 0) { 21 | setVolume(previousVolume) 22 | } else { 23 | setPreviousVolume(volume) 24 | setVolume(0) 25 | } 26 | } 27 | 28 | function handleVolumeChange(newVolume: number) { 29 | setVolume(newVolume) 30 | playClick() 31 | } 32 | 33 | return 34 | 0}`} onClick={handleVolumeToggle} variant={"contained"}> 35 | 36 | 37 | 38 | handleVolumeChange(0.25)} 39 | isvolume={`${volume >= 0.25}`}> 40 | handleVolumeChange(0.5)} isvolume={`${volume >= 0.5}`}/> 41 | handleVolumeChange(0.75)} isvolume={`${volume >= 0.75}`}/> 42 | handleVolumeChange(1)} isvolume={`${volume === 1}`}/> 43 | 44 | 45 | 46 | } 47 | 48 | const StyledVolumeBar = styled.div` 49 | height: 3rem; 50 | display: flex; 51 | align-items: center; 52 | `; 53 | 54 | const StyledButtonContainer = styled.div` 55 | height: 90%; 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | gap: 0.25rem; 60 | `; 61 | 62 | const StyledSoundButton = styled.button<{ isvolume: string }>` 63 | height: 100%; 64 | background: var(--color-${({isvolume}) => isvolume === "true" ? "primary" : "grey"}); 65 | ${({isvolume}) => isvolume === "true" ? "box-shadow: 0 0 0.5rem var(--color-primary);" : ""} 66 | border: none; 67 | `; 68 | 69 | const StyledOnOffButton = styled(Button)<{ isvolume: string }>` 70 | width: 5rem; 71 | height: 5rem; 72 | scale: 0.6; 73 | color: var(--color-${({isvolume}) => isvolume === "true" ? "primary" : "grey"}); 74 | border: 0.25rem solid var(--color-${({isvolume}) => isvolume === "true" ? "primary" : "grey"}); 75 | background: var(--color-${({isvolume}) => isvolume === "true" ? "semiblack" : "black"}); 76 | border-radius: 12px; 77 | `; 78 | -------------------------------------------------------------------------------- /frontend/src/components/css/leaflet.css: -------------------------------------------------------------------------------- 1 | .leaflet-container { 2 | border: 2px solid var(--color-semiblack); 3 | } 4 | 5 | #map { 6 | background: var(--color-black); 7 | z-index: 5; 8 | } 9 | 10 | .leaflet-container .leaflet-control-attribution { 11 | display: none; 12 | } 13 | 14 | .leaflet-control-zoom.leaflet-bar.leaflet-control a { 15 | background: var(--color-semiblack); 16 | color: var(--color-primary); 17 | border-color: var(--color-primary); 18 | } 19 | 20 | .node { 21 | border: 2px solid var(--color-grey); 22 | border-radius: 5px; 23 | } 24 | 25 | .--player { 26 | border: 2px solid var(--color-primary); 27 | } 28 | 29 | .--enemy { 30 | border: 2px solid var(--color-secondary); 31 | } 32 | 33 | .leaflet-marker-pane .player { 34 | border-radius: 50px; 35 | animation: halo-pulse 2s ease-in-out infinite; 36 | background: var(--color-black); 37 | } 38 | 39 | @keyframes halo-pulse { 40 | 0% { 41 | outline: 0.25rem solid var(--color-primary); 42 | filter: drop-shadow(0 0 2rem var(--color-primary)); 43 | } 44 | 50% { 45 | outline: 0.25rem solid transparent; 46 | filter: drop-shadow(0 0 0 var(--color-primary)); 47 | } 48 | 100% { 49 | outline: 0.25rem solid var(--color-primary); 50 | filter: drop-shadow(0 0 2rem var(--color-primary)); 51 | } 52 | } 53 | 54 | .leaflet-marker-pane .player__enemy { 55 | border-radius: 50px; 56 | } 57 | 58 | .leaflet-popup { 59 | background: transparent; 60 | } 61 | 62 | .leaflet-popup-tip-container .leaflet-popup-tip { 63 | background: var(--color-semiblack); 64 | } 65 | 66 | .leaflet-popup .leaflet-popup-content-wrapper { 67 | background: transparent; 68 | } 69 | 70 | .leaflet-popup .leaflet-popup-content-wrapper .leaflet-popup-content { 71 | background: transparent; 72 | width: 12rem; 73 | height: 7rem; 74 | margin: 0; 75 | display: flex; 76 | flex-direction: column; 77 | justify-content: center; 78 | align-items: center; 79 | } 80 | 81 | .leaflet-popup-close-button { 82 | display: none; 83 | } 84 | 85 | .player-popup .leaflet-popup-content-wrapper { 86 | padding: 0; 87 | margin: 0; 88 | } 89 | 90 | .player-popup .leaflet-popup-content-wrapper .leaflet-popup-content { 91 | width: 2rem; 92 | height: 2rem; 93 | } 94 | 95 | @keyframes pulse-update { 96 | 0% { 97 | outline: 0.25rem solid var(--color-primary); 98 | filter: drop-shadow(0 0 0.25rem var(--color-primary)); 99 | } 100 | 50% { 101 | outline: 0.25rem solid transparent; 102 | filter: drop-shadow(0 0 0 var(--color-primary)); 103 | } 104 | 100% { 105 | outline: 0.25rem solid var(--color-primary); 106 | filter: drop-shadow(0 0 0.25rem var(--color-primary)); 107 | } 108 | } 109 | 110 | @keyframes pulse-attack { 111 | 0% { 112 | outline: 0.25rem solid var(--color-secondary); 113 | filter: drop-shadow(0 0 0.25rem var(--color-secondary)); 114 | } 115 | 50% { 116 | outline: 0.25rem solid transparent; 117 | filter: drop-shadow(0 0 0 var(--color-secondary)); 118 | } 119 | 100% { 120 | outline: 0.25rem solid var(--color-secondary); 121 | filter: drop-shadow(0 0 0.25rem var(--color-secondary)); 122 | } 123 | } 124 | 125 | div.leaflet-control-layers { 126 | background: transparent; 127 | border-radius: 50%; 128 | } 129 | 130 | .leaflet-control-layers-toggle { 131 | background: var(--color-semiblack); 132 | border: 2px solid var(--color-primary); 133 | border-radius: 50%; 134 | } 135 | 136 | .leaflet-retina .leaflet-control .leaflet-control-layers-toggle { 137 | background-image: url("../../assets/svg/layers.svg"); 138 | color: var(--color-primary); 139 | 140 | } 141 | 142 | div.leaflet-control-layers-expanded { 143 | background: var(--color-semiblack); 144 | border: 2px solid var(--color-semiblack); 145 | color: var(--color-primary); 146 | border-radius: 5px; 147 | } 148 | 149 | input.leaflet-control-layers-selector:checked { 150 | accent-color: var(--color-primary); 151 | } 152 | -------------------------------------------------------------------------------- /frontend/src/components/icons/AttackIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function AttackIcon() { 4 | return 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/CharacterIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function CharacterIcon() { 4 | return 5 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function ChatIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/CpuIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function CpuIcon() { 4 | return 5 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/DowngradeIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function DowngradeIcon() { 4 | return 5 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ListIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function ListIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/MiniAttackIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function MiniAttackIcon() { 4 | return 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/MiniDowngradeIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function MiniDowngradeIcon() { 4 | return 5 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/MiniUpgradeIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function MiniUpgradeIcon() { 4 | return 5 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/NavigationIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function NavigationIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/NetworkIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function NetworkIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/PositionIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function PositionIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ScanIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function ScanIcon() { 4 | return 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/SoundIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function SoundIcon() { 4 | return 5 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/UnlockIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function UnlockIcon() { 4 | return 5 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/icons/UpgradeIcon.tsx: -------------------------------------------------------------------------------- 1 | import {SvgIcon} from "@mui/material"; 2 | 3 | export default function UpgradeIcon() { 4 | return 5 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/icons/mapIcons.ts: -------------------------------------------------------------------------------- 1 | import {Icon} from "leaflet"; 2 | import CharacterEnemy from "../../assets/svg/character_enemy.svg"; 3 | import PlayerAvatar from "../../assets/images/defaultAvatar.webp"; 4 | import DatabaseNeutral from "../../assets/svg/database_neutral.svg"; 5 | import DatabasePlayer from "../../assets/svg/databanse_player.svg"; 6 | import DatabaseEnemy from "../../assets/svg/database_enemy.svg"; 7 | import RadioTowerNeutral from "../../assets/svg/radio_neutral.svg"; 8 | import RadioTowerPlayer from "../../assets/svg/radio_player.svg"; 9 | import RadioTowerEnemy from "../../assets/svg/radio_enemy.svg"; 10 | import TradingNeutral from "../../assets/svg/trading_neutral.svg"; 11 | import TradingPlayer from "../../assets/svg/trading_player.svg"; 12 | import TradingEnemy from "../../assets/svg/trading_enemy.svg"; 13 | import CpuNeutral from "../../assets/svg/cpu_neutral.svg"; 14 | import CpuPlayer from "../../assets/svg/cpu_player.svg"; 15 | import CpuEnemy from "../../assets/svg/cpu_enemy.svg"; 16 | import CctvNeutral from "../../assets/svg/cctv_neutral.svg"; 17 | import CctvPlayer from "../../assets/svg/cctv_player.svg"; 18 | import CctvEnemy from "../../assets/svg/cctv_enemy.svg"; 19 | 20 | export const nodeIcon = (nodeName: string, owner: string, status: string, inRange: boolean) => { 21 | const className = buildClass(owner, status, inRange) 22 | const iconUrl = getNodeTypeIcon(nodeName, owner) 23 | return new Icon({iconUrl: iconUrl, iconSize: [32, 32], className: className}) 24 | } 25 | 26 | function buildClass(owner: string, status: string, inRange: boolean) { 27 | const baseClass = "node " 28 | let ownerClass = ""; 29 | if (inRange) { 30 | switch (owner) { 31 | case "player": 32 | ownerClass = " --player" 33 | break; 34 | case "enemy": 35 | ownerClass = " --enemy" 36 | break; 37 | default: 38 | break; 39 | } 40 | } 41 | return baseClass + ownerClass + (status !== "none" && ` --${status}`) 42 | } 43 | 44 | function getNodeTypeIcon(nodeName: string, owner: string) { 45 | const defaultNames = ["Trading interface", "Server farm", "Database access", "CCTV control"]; 46 | if (!defaultNames.includes(nodeName)) { 47 | return getDefaultIcon(owner) 48 | } else { 49 | if (nodeName === "Trading interface") { 50 | return getTradingIcon(owner) 51 | } else if (nodeName === "Server farm") { 52 | return getCpuIcon(owner) 53 | } else if (nodeName === "Database access") { 54 | return getDatabaseIcon(owner) 55 | } else { 56 | return getCctvIcon(owner) 57 | } 58 | } 59 | } 60 | 61 | function getDefaultIcon(owner: string) { 62 | switch (owner) { 63 | case "player": 64 | return RadioTowerPlayer 65 | case "enemy": 66 | return RadioTowerEnemy 67 | default: 68 | return RadioTowerNeutral 69 | } 70 | } 71 | 72 | function getTradingIcon(owner: string) { 73 | switch (owner) { 74 | case "player": 75 | return TradingPlayer 76 | case "enemy": 77 | return TradingEnemy 78 | default: 79 | return TradingNeutral 80 | } 81 | } 82 | 83 | function getCpuIcon(owner: string) { 84 | switch (owner) { 85 | case "player": 86 | return CpuPlayer 87 | case "enemy": 88 | return CpuEnemy 89 | default: 90 | return CpuNeutral 91 | } 92 | } 93 | 94 | function getDatabaseIcon(owner: string) { 95 | switch (owner) { 96 | case "player": 97 | return DatabasePlayer 98 | case "enemy": 99 | return DatabaseEnemy 100 | default: 101 | return DatabaseNeutral 102 | } 103 | } 104 | 105 | function getCctvIcon(owner: string) { 106 | switch (owner) { 107 | case "player": 108 | return CctvPlayer 109 | case "enemy": 110 | return CctvEnemy 111 | default: 112 | return CctvNeutral 113 | } 114 | } 115 | 116 | export const playerIcon = (userName: string, playerName: string) => { 117 | if (userName === playerName) { 118 | return new Icon({iconUrl: PlayerAvatar, iconSize: [32, 32], className: "player"}) 119 | } else { 120 | return new Icon({iconUrl: CharacterEnemy, iconSize: [32, 32], className: "player__enemy"}) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledButtonContainer.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledButtonContainer = styled.div` 4 | display: flex; 5 | align-items: flex-start; 6 | justify-content: center; 7 | position: fixed; 8 | bottom: 0; 9 | width: 100%; 10 | height: 5rem; 11 | gap: 0.25rem; 12 | z-index: 6; 13 | background: linear-gradient(transparent, var(--color-black) 50%); 14 | `; 15 | -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledForm.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledForm = styled.form` 4 | margin-top: 1.5rem; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | width: 90%; 10 | 11 | legend { 12 | font-size: 1.5rem; 13 | } 14 | `; -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledFormButton.ts: -------------------------------------------------------------------------------- 1 | import {Button} from "@mui/material"; 2 | import styled from "@emotion/styled"; 3 | 4 | export const StyledFormButton = styled(Button)<{theme: string}>` 5 | background: var(--color-${props => props.theme === "error" ? "black" : "semiblack"}); 6 | border: 4px solid var(--color-${props => props.theme === "error" ? "secondary" : "primary"}); 7 | border-radius: 12px; 8 | color: var(--color-${props => props.theme === "error" ? "secondary" : "primary"}); 9 | font: inherit; 10 | width: 45%; 11 | filter: drop-shadow(0 0 0.15rem var(--color-${props => props.theme === "error" ? "secondary" : "primary"})); 12 | text-shadow: 0 0 0.15rem var(--color-${props => props.theme === "error" ? "secondary" : "primary"}); 13 | 14 | &:disabled { 15 | background: var(--color-black); 16 | border-color: var(--color-semiblack); 17 | color: var(--color-semiblack); 18 | filter: none; 19 | text-shadow: none; 20 | } 21 | 22 | &:hover { 23 | background: var(--color-${props => props.theme === "error" ? "black" : "semiblack"}); 24 | } 25 | 26 | &:active { 27 | scale: 0.95; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledHelperContainer.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledHelperContainer = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | width: 100%; 7 | height: 1.5rem; 8 | `; -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledHelperText.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled" 2 | 3 | export const StyledHelperText = styled.small` 4 | color: var(--color-secondary); 5 | font-family: inherit; 6 | font-size: 1rem; 7 | `; -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledInput.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledInput = styled.input` 4 | background: var(--color-semiblack); 5 | outline: 2px solid var(--color-primary); 6 | border: none; 7 | color: var(--color-secondary); 8 | font: inherit; 9 | height: 3rem; 10 | font-size: 1.5rem; 11 | text-align: center; 12 | text-shadow: 0 0 5px var(--color-secondary); 13 | 14 | &::placeholder { 15 | color: var(--color-secondary); 16 | } 17 | 18 | @media (min-width: 768px) { 19 | width: 50%; 20 | } 21 | 22 | @media (min-width: 1024px) { 23 | width: 30%; 24 | } 25 | `; -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledLabel.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledLabel = styled.label` 4 | color: var(--color-primary); 5 | font-size: 1.5rem; 6 | `; -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledStatusBar.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const StyledStatusBar = styled.div` 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | width: 100%; 8 | background-color: var(--color-black); 9 | opacity: 0.8; 10 | z-index: 10; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | padding: 0 0.5rem; 15 | `; 16 | -------------------------------------------------------------------------------- /frontend/src/components/styled/StyledToastContainer.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import {ToastContainer} from "react-toastify"; 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | 5 | export const StyledToastContainer = styled(ToastContainer)` 6 | .Toastify__toast { 7 | text-align: center; 8 | width: 100%; 9 | max-width: 50rem; 10 | font-family: inherit; 11 | } 12 | 13 | .Toastify__toast--error { 14 | background-color: var(--color-secondary); 15 | color: black; 16 | filter: drop-shadow(0 0 0.75rem var(--color-secondary)); 17 | 18 | .Toastify__progress-bar { 19 | background-color: var(--color-primary); 20 | } 21 | } 22 | 23 | .Toastify__toast--success { 24 | background-color: var(--color-primary); 25 | color: black; 26 | filter: drop-shadow(0 0 0.75rem var(--color-primary)); 27 | 28 | .Toastify__progress-bar { 29 | background-color: var(--color-secondary); 30 | } 31 | } 32 | 33 | .Toastify__close-button { 34 | opacity: 1; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /frontend/src/hooks/useCooldown.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | 3 | export default function useCooldown(lastActionTimestamp: number, cooldownDuration: number) { 4 | const [isOnCooldown, setIsOnCooldown] = useState(false); 5 | const [counter, setCounter] = useState(0); 6 | 7 | useEffect(() => { 8 | 9 | const now = Math.floor(Date.now() / 1000); 10 | const remainingCooldown = Math.max(0, lastActionTimestamp + cooldownDuration - now); 11 | 12 | if (remainingCooldown > 0) { 13 | setIsOnCooldown(true); 14 | 15 | const cooldownInterval = setInterval(() => { 16 | const updatedCooldown = Math.max(0, lastActionTimestamp + cooldownDuration - Math.floor(Date.now() / 1000)); 17 | setCounter(updatedCooldown); 18 | if (updatedCooldown === 0) { 19 | setIsOnCooldown(false); 20 | clearInterval(cooldownInterval); 21 | } 22 | }, 1000); 23 | 24 | return () => clearInterval(cooldownInterval); 25 | } 26 | }, [cooldownDuration, lastActionTimestamp]); 27 | 28 | return { isOnCooldown, counter }; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/hooks/useNodes.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import axios from 'axios'; 3 | import {Node, Player} from "../models.ts"; 4 | 5 | export default function useNodes(ownerId: string, currentPlayer: Player | null) { 6 | const [nodes, setNodes] = useState(); 7 | 8 | useEffect(() => { 9 | if (!ownerId || !currentPlayer) { 10 | return 11 | } 12 | axios 13 | .get(ownerId && `/api/nodes/${ownerId}`) 14 | .then((response) => response.data) 15 | .then((data: Node[]) => { 16 | setNodes(data); 17 | }) 18 | .catch(console.error); 19 | }, [ownerId, currentPlayer]); 20 | 21 | return nodes; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/hooks/useOwner.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import axios from 'axios'; 3 | 4 | export default function useOwner(ownerId: string) { 5 | const [owner, setOwner] = useState(''); 6 | 7 | useEffect(() => { 8 | if (!ownerId) { 9 | setOwner(''); 10 | return; 11 | } 12 | 13 | axios 14 | .get(`/api/player/${ownerId}`) 15 | .then((response) => response.data) 16 | .then((data: string) => { 17 | setOwner(data || ''); 18 | }) 19 | .catch(() => setOwner('')); 20 | }, [ownerId]); 21 | 22 | return owner; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/hooks/usePlayer.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import axios from 'axios'; 3 | import {Player} from "../models.ts"; 4 | 5 | export default function usePlayer(name: string) { 6 | const [player, setPlayer] = useState(); 7 | 8 | useEffect(() => { 9 | if (!name) { 10 | return 11 | } 12 | axios 13 | .get(name && `/api/player/info/${name}`) 14 | .then((response) => response.data) 15 | .then((data: Player) => { 16 | setPlayer(data); 17 | }) 18 | .catch(console.error); 19 | }, [name]); 20 | 21 | return player; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | background-color: #1e1e1e; 8 | 9 | font-synthesis: none; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-text-size-adjust: 100%; 14 | 15 | --color-black: #1e1e1e; 16 | --color-semiblack: #343a40; 17 | --color-grey: #868e96; 18 | --color-primary: #01fae6; 19 | --color-secondary: #ff004f; 20 | } 21 | 22 | *, *::before, *::after { 23 | box-sizing: border-box; 24 | } 25 | 26 | * { 27 | margin: 0; 28 | } 29 | 30 | a { 31 | font-weight: 500; 32 | color: #646cff; 33 | text-decoration: inherit; 34 | } 35 | 36 | a:hover { 37 | color: #535bf2; 38 | } 39 | 40 | body { 41 | margin: 0; 42 | min-width: 320px; 43 | line-height: 1.5; 44 | -webkit-font-smoothing: antialiased; 45 | } 46 | 47 | img, picture, video, canvas, svg { 48 | display: block; 49 | max-width: 100%; 50 | } 51 | 52 | input, button, textarea, select { 53 | font: inherit; 54 | } 55 | 56 | p, h1, h2, h3, h4, h5, h6 { 57 | overflow-wrap: break-word; 58 | } 59 | 60 | #root, #__next { 61 | isolation: isolate; 62 | } 63 | 64 | /* 65 | @media (prefers-color-scheme: light) { 66 | :root { 67 | color: #213547; 68 | background-color: #ffffff; 69 | } 70 | 71 | a:hover { 72 | color: #747bff; 73 | } 74 | 75 | button { 76 | background-color: #f9f9f9; 77 | } 78 | } 79 | */ 80 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | import "./components/css/leaflet.css" 6 | import 'leaflet/dist/leaflet.css' 7 | import {BrowserRouter} from "react-router-dom"; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')!).render( 10 | 11 | 12 | 13 | 14 | , 15 | ) 16 | -------------------------------------------------------------------------------- /frontend/src/models.ts: -------------------------------------------------------------------------------- 1 | export type Player = { 2 | id: string, 3 | userId: string, 4 | name: string, 5 | coordinates: Coordinates, 6 | level: number, 7 | experience: number, 8 | experienceToNextLevel: number, 9 | health: number, 10 | maxHealth: number, 11 | attack: number, 12 | maxAttack: number, 13 | credits: number, 14 | lastScan: number 15 | } 16 | 17 | export type Node = { 18 | id: string, 19 | ownerId: string, 20 | name: string, 21 | level: number, 22 | health: number, 23 | lastUpdate: number, 24 | lastAttack: number, 25 | coordinates: Coordinates, 26 | } 27 | 28 | export type NodeData = { 29 | name: string, 30 | coordinates: Coordinates 31 | } 32 | 33 | export type Coordinates = { 34 | latitude: number, 35 | longitude: number, 36 | timestamp: number 37 | } 38 | 39 | export type Message = { 40 | username: string, 41 | message: string, 42 | timestamp: number 43 | } 44 | 45 | export enum ActionType { 46 | HACK = "HACK", 47 | ABANDON = "ABANDON", 48 | } 49 | 50 | export enum ItemSize { 51 | SMALL = "SMALL", 52 | MEDIUM = "MEDIUM", 53 | LARGE = "LARGE", 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/theme.ts: -------------------------------------------------------------------------------- 1 | import {createTheme} from "@mui/material"; 2 | import NerdFont from "./assets/fonts/3270/3270NerdFont-Regular.ttf"; 3 | 4 | export const theme = createTheme({ 5 | palette: { 6 | primary: { 7 | main: "#343a40", 8 | light: "#868e96", 9 | dark: "#1e1e1e" 10 | }, 11 | error: { 12 | main: "#ff004f", 13 | }, 14 | success: { 15 | main: "#01fae6", 16 | } 17 | }, 18 | typography: { 19 | fontFamily: "3270" 20 | }, 21 | components: { 22 | MuiCssBaseline: { 23 | styleOverrides: ` 24 | @font-face { 25 | font-family: "3270"; 26 | src: url(${NerdFont}); 27 | `, 28 | } 29 | } 30 | }) -------------------------------------------------------------------------------- /frontend/src/utils/calculation.ts: -------------------------------------------------------------------------------- 1 | import {getDistance} from "geolib"; 2 | 3 | export function getSecondsSinceTimestamp(timestamp: number): number { 4 | return Math.floor((Date.now() / 1000 - timestamp)); 5 | } 6 | 7 | export function getDistanceBetweenCoordinates( 8 | start: { latitude: number, longitude: number }, 9 | end: { latitude: number, longitude: number } 10 | ): number { 11 | return getDistance(start, end); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/utils/getDistanceString.ts: -------------------------------------------------------------------------------- 1 | export default function getDistanceString(distance: number) { 2 | if (distance < 1000) { 3 | return `${distance}m` 4 | } else { 5 | return `${(distance / 1000).toFixed(1)}km` 6 | } 7 | } -------------------------------------------------------------------------------- /frontend/src/utils/sound.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import useSound from "use-sound"; 4 | import upgrade from "../assets/sounds/upgrade.mp3"; 5 | import click from "../assets/sounds/click.mp3"; 6 | import loginSuccess from "../assets/sounds/login_success.mp3"; 7 | import error from "../assets/sounds/error.mp3"; 8 | import electricMachine from "../assets/sounds/electric_machine.mp3"; 9 | import zoomIn from "../assets/sounds/zoom_in.mp3"; 10 | import zoomOut from "../assets/sounds/zoom_out.mp3"; 11 | import switchSound from "../assets/sounds/switch.mp3"; 12 | import keyPress from "../assets/sounds/key_press.mp3"; 13 | import loadingOs from "../assets/sounds/loading_os.mp3"; 14 | import {useStore} from "../hooks/useStore.ts"; 15 | 16 | export function useUpgradeSound() { 17 | const volume = useStore.getState().volume; 18 | const [play] = useSound(upgrade, { volume }); 19 | return play; 20 | } 21 | 22 | export function useClickSound() { 23 | const volume = useStore.getState().volume; 24 | const [play] = useSound(click, { volume }); 25 | return play; 26 | } 27 | 28 | export function useLoginSuccessSound() { 29 | const volume = useStore.getState().volume; 30 | const [play] = useSound(loginSuccess, { volume }); 31 | return play; 32 | } 33 | 34 | export function useErrorSound() { 35 | const volume = useStore.getState().volume; 36 | const [play] = useSound(error, { volume }); 37 | return play; 38 | } 39 | 40 | export function useElectricMachineSound() { 41 | const volume = useStore.getState().volume; 42 | const [play] = useSound(electricMachine, { volume }); 43 | return play; 44 | } 45 | 46 | export function useZoomInSound() { 47 | const volume = useStore.getState().volume; 48 | const [play] = useSound(zoomIn, { volume }); 49 | return play; 50 | } 51 | 52 | export function useZoomOutSound() { 53 | const volume = useStore.getState().volume; 54 | const [play] = useSound(zoomOut, { volume }); 55 | return play; 56 | } 57 | 58 | export function useSwitchSound() { 59 | const volume = useStore.getState().volume; 60 | const [play] = useSound(switchSound, { volume }); 61 | return play; 62 | } 63 | 64 | export function useKeyPressSound() { 65 | const volume = useStore.getState().volume; 66 | const [play] = useSound(keyPress, { volume }); 67 | return play; 68 | } 69 | 70 | export function useLoadingOsSound() { 71 | const volume = useStore.getState().volume; 72 | const [play] = useSound(loadingOs, { volume }); 73 | return play; 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: 'http://localhost:8080', 11 | } 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | < Explain the motivation behind your change > 3 | 4 | # Implementation 5 | < Explain how you implemented your change > 6 | 7 | # Testing 8 | < Did you add new tests? > 9 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=netRunner_frontend 2 | sonar.organization=toshydev 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=netRunner_frontend 6 | #sonar.projectVersion=1.0 7 | 8 | 9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 10 | sonar.sources=./frontend/src 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | #sonar.sourceEncoding=UTF-8 14 | sonar.coverage.exclusions=frontend/**/*.* --------------------------------------------------------------------------------