├── .clang-format ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── codeql │ └── config.yml └── workflows │ ├── codeql.yml │ ├── deploy-auth-server.yml │ ├── deploy-game.yml │ ├── deploy-nginx.yml │ ├── deploy-random.yml │ ├── deploy-ssh-gateway.yml │ ├── graphviz.yml │ └── upload-assets.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets ├── achievements │ ├── 3x_doubles.png │ ├── 5x_doubles.png │ ├── 7x_doubles.png │ ├── addicted.png │ ├── astronaut.png │ ├── beginners_luck.png │ ├── getting_started.png │ ├── good_job.png │ ├── negative.png │ ├── oops.png │ ├── perfect.png │ ├── rude.png │ └── thief.png ├── ammo.js └── badges │ ├── admin.png │ └── premium.gif ├── auth ├── .cargo │ └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── achievements.json ├── badges.json ├── build.rs ├── devdb │ └── docker-compose.yml ├── docker-compose.yml ├── migrations │ ├── V10__donors.sql │ ├── V11__payments.sql │ ├── V12__pubkeys.sql │ ├── V13__pubkey_settings.sql │ ├── V14__dice_type_strings.sql │ ├── V15__histogram_tracking.sql │ ├── V16__bigger_ints.sql │ ├── V17__winner_histograms.sql │ ├── V1__initial.sql │ ├── V2__drop_passwords.sql │ ├── V3__snake_case_everything.sql │ ├── V4__drop_achievements.sql │ ├── V5__default_username.sql │ ├── V6__anon_identity.sql │ ├── V7__cascades.sql │ ├── V8__settings.sql │ └── V9__player_colors.sql ├── src │ ├── generated.rs │ ├── lib.rs │ ├── main.rs │ ├── migrations.rs │ └── routes │ │ ├── server_routes.rs │ │ ├── user_routes.rs │ │ └── webhook_routes.rs └── tasks.toml ├── client ├── .gitignore ├── .prettierrc ├── functions │ ├── room │ │ └── [room_id].ts │ └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── Icosphere.png │ ├── crown.png │ ├── default_player.png │ ├── dice.png │ ├── emissive.png │ ├── favicon.ico │ ├── gear.png │ ├── gold6.png │ ├── hands.png │ ├── itsboats.png │ ├── logo192.png │ ├── manifest.json │ ├── normal6.png │ ├── normal6_2.png │ ├── robots.txt │ ├── simulated.txt │ └── skybox │ │ ├── room.hdr │ │ ├── skybox_nx.jpg │ │ ├── skybox_ny.jpg │ │ ├── skybox_nz.jpg │ │ ├── skybox_px.jpg │ │ ├── skybox_py.jpg │ │ └── skybox_pz.jpg ├── src │ ├── 3d │ │ └── main.ts │ ├── App.css │ ├── App.tsx │ ├── actions │ │ └── settings.ts │ ├── api │ │ └── auth.ts │ ├── connection.tsx │ ├── constants.ts │ ├── hooks │ │ └── window_size.ts │ ├── index.css │ ├── index.tsx │ ├── pages │ │ ├── game_page.tsx │ │ ├── home_page.tsx │ │ ├── login.css │ │ ├── onboard_page.tsx │ │ ├── settings_page.tsx │ │ ├── tab_error_page.tsx │ │ └── twitch_oauth_page.tsx │ ├── providers │ │ ├── achievements.tsx │ │ ├── auth.tsx │ │ └── badges.tsx │ ├── react-app-env.d.ts │ ├── reducers │ │ ├── auth.ts │ │ ├── chat.ts │ │ ├── connection.ts │ │ ├── game.ts │ │ ├── pop_text.ts │ │ ├── settings.ts │ │ └── themes.ts │ ├── selectors │ │ └── game_selectors.ts │ ├── stitches.config.ts │ ├── store.ts │ ├── textmods.css │ ├── toastify.css │ ├── twitch.tsx │ ├── types │ │ └── api.ts │ └── ui │ │ ├── achievement.css │ │ ├── achievement.tsx │ │ ├── auth_menu.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── buttons │ │ ├── add_sub_button.tsx │ │ ├── button.tsx │ │ ├── buttons.css │ │ ├── octocat.tsx │ │ ├── restart_button.tsx │ │ ├── roll_button.tsx │ │ ├── slider.tsx │ │ ├── textarea.tsx │ │ └── toggle.tsx │ │ ├── chat.tsx │ │ ├── cheat_sheet.tsx │ │ ├── conn_banner.tsx │ │ ├── dice.css │ │ ├── dice.png │ │ ├── dice.tsx │ │ ├── die.tsx │ │ ├── floating_bar.tsx │ │ ├── game_panel.tsx │ │ ├── icons │ │ ├── disconnected.tsx │ │ ├── help.tsx │ │ └── kick.tsx │ │ ├── player.tsx │ │ ├── players.tsx │ │ ├── poptext.tsx │ │ └── settings.tsx ├── tasks.toml ├── tsconfig.json ├── types │ └── react-inline-editing.d.ts └── vite.config.ts ├── docs ├── .nojekyll ├── CNAME ├── README.md ├── _sidebar.md ├── gen.md ├── gen │ ├── .nojekyll │ ├── interfaces │ │ ├── server_messages.ChatMsg.md │ │ ├── server_messages.DisconnectMsg.md │ │ ├── server_messages.GameError.md │ │ ├── server_messages.GameState.md │ │ ├── server_messages.IGameState.md │ │ ├── server_messages.JoinMsg.md │ │ ├── server_messages.KickMsg.md │ │ ├── server_messages.ReconnectMsg.md │ │ ├── server_messages.Redirect.md │ │ ├── server_messages.RestartMsg.md │ │ ├── server_messages.RollAgainMsg.md │ │ ├── server_messages.RollMsg.md │ │ ├── server_messages.Room.md │ │ ├── server_messages.RoomList.md │ │ ├── server_messages.UpdateMsg.md │ │ ├── server_messages.UpdateNameMsg.md │ │ ├── server_messages.UpdateTurnMsg.md │ │ ├── server_messages.WelcomeMsg.md │ │ ├── server_messages.WinMsg.md │ │ ├── store_types.Achievement.md │ │ ├── store_types.AchievementProgress.md │ │ ├── store_types.AchievementUnlock.md │ │ ├── store_types.DieRoll.md │ │ ├── store_types.Player.md │ │ ├── store_types.ReportStats.md │ │ ├── store_types.ServerPlayer.md │ │ ├── store_types.UserData.md │ │ └── store_types.UserStats.md │ ├── modules.md │ └── modules │ │ ├── server_messages.md │ │ └── store_types.md ├── index.html └── system-graph.png ├── flake.lock ├── flake.nix ├── game ├── .ccls ├── .ccls-root ├── .gitignore ├── Dockerfile ├── Makefile ├── compile_flags.txt ├── docker-compose.yml ├── includes │ ├── HTTPRequest.hpp │ ├── base.h │ ├── defaults.h │ ├── json.hpp │ ├── jwt.h │ └── traits.h ├── run-server.sh ├── src │ ├── API.cpp │ ├── API.h │ ├── AuthServerRequestQueue.cpp │ ├── AuthServerRequestQueue.h │ ├── Consts.h │ ├── Game.cpp │ ├── Game.h │ ├── GameCoordinator.cpp │ ├── GameCoordinator.h │ ├── GameServer.cpp │ ├── HTTPClient.cpp │ ├── HTTPClient.h │ ├── JWTVerifier.cpp │ ├── JWTVerifier.h │ ├── Metrics.h │ ├── RequestQueue.h │ ├── RichTextStream.h │ ├── RngOverTcp.h │ ├── StringUtils.cpp │ ├── StringUtils.h │ ├── achievements │ │ ├── All.cpp │ │ ├── All.h │ │ ├── Astronaut.h │ │ ├── BaseAchievement.h │ │ ├── Doubles.h │ │ ├── GettingStarted.h │ │ ├── Negative.h │ │ ├── Oops.h │ │ ├── Perfect.h │ │ ├── Rude.h │ │ ├── Thief.h │ │ └── WinGames.h │ └── api │ │ ├── API.cpp │ │ ├── API.hpp │ │ ├── Achievement.hpp │ │ ├── AchievementData.hpp │ │ ├── AchievementProgress.hpp │ │ ├── AchievementProgressType.hpp │ │ ├── AchievementUnlock.hpp │ │ ├── AchievementUnlockType.hpp │ │ ├── Alignment.hpp │ │ ├── AuthDonateResponse.hpp │ │ ├── AuthRefreshTokenResponse.hpp │ │ ├── AuthSettingsRequest.hpp │ │ ├── ChatMsg.hpp │ │ ├── ChatMsgType.hpp │ │ ├── Color.hpp │ │ ├── Dice.hpp │ │ ├── DiceType.hpp │ │ ├── DieRoll.hpp │ │ ├── DisconnectMsg.hpp │ │ ├── DisconnectMsgType.hpp │ │ ├── GameError.hpp │ │ ├── GameErrorType.hpp │ │ ├── GameState.hpp │ │ ├── GameStateType.hpp │ │ ├── IGameState.hpp │ │ ├── JoinMsg.hpp │ │ ├── JoinMsgType.hpp │ │ ├── KickMsg.hpp │ │ ├── KickMsgType.hpp │ │ ├── LoginRequest.hpp │ │ ├── Modifier.hpp │ │ ├── MsgElement.hpp │ │ ├── Player.hpp │ │ ├── ReconnectMsg.hpp │ │ ├── ReconnectMsgType.hpp │ │ ├── Redirect.hpp │ │ ├── RedirectType.hpp │ │ ├── RefetchPlayerMsg.hpp │ │ ├── RefetchPlayerMsgType.hpp │ │ ├── ReportStats.hpp │ │ ├── RestartMsg.hpp │ │ ├── RestartMsgType.hpp │ │ ├── RichTextChunk.hpp │ │ ├── RichTextChunkType.hpp │ │ ├── RichTextMsg.hpp │ │ ├── RichTextMsgType.hpp │ │ ├── RollAgainMsg.hpp │ │ ├── RollAgainMsgType.hpp │ │ ├── RollMsg.hpp │ │ ├── RollMsgType.hpp │ │ ├── Room.hpp │ │ ├── RoomListMsg.hpp │ │ ├── RoomListMsgType.hpp │ │ ├── ServerMsg.hpp │ │ ├── ServerMsgMsg.hpp │ │ ├── ServerMsgType.hpp │ │ ├── ServerPlayer.hpp │ │ ├── Setting.hpp │ │ ├── SpectatorsMsg.hpp │ │ ├── SpectatorsMsgType.hpp │ │ ├── UpdateMsg.hpp │ │ ├── UpdateMsgType.hpp │ │ ├── UpdateNameMsg.hpp │ │ ├── UpdateNameMsgType.hpp │ │ ├── UpdateTurnMsg.hpp │ │ ├── UpdateTurnMsgType.hpp │ │ ├── UserData.hpp │ │ ├── UserId.hpp │ │ ├── UserIdType.hpp │ │ ├── UserStats.hpp │ │ ├── WelcomeMsg.hpp │ │ ├── WelcomeMsgType.hpp │ │ ├── WinMsg.hpp │ │ ├── WinMsgType.hpp │ │ └── helper.hpp ├── tasks.toml └── uWebSockets │ ├── LICENSE │ ├── README.md │ ├── src │ ├── App.h │ ├── AsyncSocket.h │ ├── AsyncSocketData.h │ ├── BloomFilter.h │ ├── HttpContext.h │ ├── HttpContextData.h │ ├── HttpParser.h │ ├── HttpResponse.h │ ├── HttpResponseData.h │ ├── HttpRouter.h │ ├── Loop.h │ ├── LoopData.h │ ├── MessageParser.h │ ├── MoveOnlyFunction.h │ ├── Multipart.h │ ├── PerMessageDeflate.h │ ├── ProxyParser.h │ ├── QueryParser.h │ ├── TopicTree.h │ ├── Utilities.h │ ├── WebSocket.h │ ├── WebSocketContext.h │ ├── WebSocketContextData.h │ ├── WebSocketData.h │ ├── WebSocketExtensions.h │ ├── WebSocketHandshake.h │ └── WebSocketProtocol.h │ └── uSockets │ ├── LICENSE │ ├── Makefile │ ├── README.md │ └── src │ ├── bsd.c │ ├── context.c │ ├── crypto │ ├── openssl.c │ ├── sni_tree.cpp │ └── wolfssl.c │ ├── eventing │ ├── asio.cpp │ ├── epoll_kqueue.c │ ├── gcd.c │ └── libuv.c │ ├── internal │ ├── eventing │ │ ├── asio.h │ │ ├── epoll_kqueue.h │ │ ├── gcd.h │ │ └── libuv.h │ ├── internal.h │ ├── loop_data.h │ └── networking │ │ └── bsd.h │ ├── libusockets.h │ ├── loop.c │ └── socket.c ├── nginx ├── config │ ├── auth.conf │ ├── game-beta.conf │ ├── game-prod.conf │ └── status.conf ├── docker-compose.yml ├── nginx-certbot.env ├── rolly_cubes_live └── rolly_cubes_live_auth ├── package-lock.json ├── package.json ├── random ├── .gitignore ├── Dockerfile ├── LICENSES ├── Makefile ├── README.md ├── docker-build.sh ├── docker-compose.yml ├── docker-run.sh └── src │ ├── DemoClient.f90 │ ├── Random.f90 │ ├── RandomServer.f90 │ └── mod_dill.f90 ├── schema.json ├── scripts ├── codespace-setup ├── create-dev-keys ├── db_backup ├── gen-types └── setup-nix ├── ssh-gateway ├── .gitignore ├── Dockerfile ├── TODO.txt ├── api │ ├── api_methods.go │ └── generated.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go └── user │ ├── context.go │ └── user.go ├── stats └── simulation.js ├── system.dot └── tasks.toml /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AllowShortIfStatementsOnASingleLine: 'true' 3 | ColumnLimit: '0' 4 | IndentCaseLabels: 'false' 5 | IndentWidth: '4' 6 | NamespaceIndentation: All 7 | 8 | ... 9 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:ubuntu 2 | 3 | # install stuff 4 | RUN apt update && \ 5 | apt install -y manpages-dev software-properties-common inotify-tools netcat 6 | 7 | RUN apt install curl -y 8 | RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \ 9 | --extra-conf "sandbox = false" \ 10 | --init none \ 11 | --no-confirm 12 | ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin" 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerComposeFile": "docker-compose.yml", 3 | "service": "devcontainer", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "updateContentCommand": "bash -c '/workspaces/${localWorkspaceFolderBasename}/scripts/codespace-setup 2>&1 | tee /workspaces/${localWorkspaceFolderBasename}/.setup-log.log'", 6 | "postCreateCommand": "bash -c '/workspaces/${localWorkspaceFolderBasename}/scripts/create-dev-keys &>> /workspaces/${localWorkspaceFolderBasename}/.setup-log.log'; git pull origin main --rebase", 7 | "forwardPorts": [3000] 8 | } 9 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | devcontainer: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | volumes: 8 | - ../..:/workspaces:cached 9 | network_mode: service:db 10 | command: sleep infinity 11 | 12 | db: 13 | image: postgres:latest 14 | restart: unless-stopped 15 | ports: 16 | - '5432:5432' 17 | volumes: 18 | - postgres-data:/var/lib/postgresql/data 19 | environment: 20 | POSTGRES_PASSWORD: test 21 | POSTGRES_USER: test 22 | POSTGRES_DB: test 23 | 24 | volumes: 25 | postgres-data: 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | game/uWebSockets/** linguist-vendored 2 | game/includes/** linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cgsdev0 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 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: enhancement 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/codeql/config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL config" 2 | 3 | paths-ignore: 4 | - game/includes 5 | - game/uWebSockets 6 | - game/api 7 | - game/prometheus-cpp 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | workflow_dispatch: {} 5 | # push: 6 | # branches: [ 'main' ] 7 | # pull_request: 8 | # The branches below must be a subset of the branches above 9 | # branches: [ 'main' ] 10 | # schedule: 11 | # - cron: '44 14 * * 3' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'cpp', 'javascript' ] 26 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 27 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | with: 33 | submodules: recursive 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | 44 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 45 | queries: +security-and-quality 46 | - run: scripts/setup-dev-env 47 | - name: Build 48 | working-directory: game 49 | run: make CXX=g++-10 CC=gcc-10 -j$(nproc) 50 | 51 | - name: Perform CodeQL Analysis 52 | uses: github/codeql-action/analyze@v2 53 | with: 54 | category: "/language:${{matrix.language}}" 55 | config-file: ./.github/codeql/config.yml 56 | -------------------------------------------------------------------------------- /.github/workflows/deploy-auth-server.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Auth Server 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | CHANNEL: latest 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: auth 16 | url: https://auth.rollycubes.com 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: docker/setup-buildx-action@v2 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@v2 26 | with: 27 | registry: ${{ env.REGISTRY }} 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - run: cp schema.json auth/schema.json 32 | - name: Build and push Docker image 33 | uses: docker/build-push-action@v4 34 | with: 35 | context: auth 36 | push: true 37 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-auth:${{ env.CHANNEL }} 38 | cache-from: type=gha 39 | cache-to: type=gha,mode=max 40 | 41 | 42 | - name: Install SSH key 43 | uses: shimataro/ssh-key-action@v2 44 | with: 45 | key: ${{ secrets.DO_SSH_KEY }} 46 | known_hosts: ${{ secrets.DO_KNOWN_HOSTS }} 47 | 48 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} mkdir -p /root/auth" 49 | - run: "scp -P 22222 auth/docker-compose.yml root@${{ secrets.DO_SERVER_IP }}:/root/auth/." 50 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} bash -c \"'docker-compose -f auth/docker-compose.yml pull && docker-compose -f auth/docker-compose.yml --env-file secrets/.env up -d'\"" 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy-nginx.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Nginx 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | packages: write 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install SSH key 17 | uses: shimataro/ssh-key-action@v2 18 | with: 19 | key: ${{ secrets.DO_SSH_KEY }} 20 | known_hosts: ${{ secrets.DO_KNOWN_HOSTS }} 21 | 22 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} bash -c \"'rm -rf /root/nginx; mkdir -p /root/nginx'\"" 23 | - run: "scp -P 22222 -r nginx/* root@${{ secrets.DO_SERVER_IP }}:/root/nginx/." 24 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} docker-compose -f nginx/docker-compose.yml up -d" 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy-random.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Random 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }}-random 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 26 | with: 27 | registry: ${{ env.REGISTRY }} 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Build and push Docker image 32 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 33 | with: 34 | context: random 35 | push: true 36 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 37 | 38 | # Deploy it 39 | - name: Install SSH key 40 | uses: shimataro/ssh-key-action@v2 41 | with: 42 | key: ${{ secrets.DO_SSH_KEY }} 43 | known_hosts: ${{ secrets.DO_KNOWN_HOSTS }} 44 | 45 | - name: copy the random server 46 | run: "scp -P 22222 random/docker-compose.yml root@${{ secrets.DO_SERVER_IP }}:/root/random" 47 | - name: start the new server 48 | run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} bash -c \"'docker-compose -f random/docker-compose.yml pull && docker-compose -f random/docker-compose.yml up -d'\"" 49 | -------------------------------------------------------------------------------- /.github/workflows/deploy-ssh-gateway.yml: -------------------------------------------------------------------------------- 1 | name: Deploy SSH Gateway 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | CHANNEL: latest 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: docker/setup-buildx-action@v2 21 | - name: Log in to the Container registry 22 | uses: docker/login-action@v2 23 | with: 24 | registry: ${{ env.REGISTRY }} 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Build and push Docker image 29 | uses: docker/build-push-action@v4 30 | with: 31 | context: ssh-gateway 32 | push: true 33 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ssh-gateway:${{ env.CHANNEL }} 34 | cache-from: type=gha 35 | cache-to: type=gha,mode=max 36 | 37 | - name: Install SSH key 38 | uses: shimataro/ssh-key-action@v2 39 | with: 40 | key: ${{ secrets.DO_SSH_KEY }} 41 | known_hosts: ${{ secrets.DO_KNOWN_HOSTS }} 42 | 43 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} mkdir -p /root/ssh-gateway" 44 | - run: "scp -P 22222 ssh-gateway/docker-compose.yml root@${{ secrets.DO_SERVER_IP }}:/root/ssh-gateway/." 45 | - run: "ssh -p 22222 root@${{ secrets.DO_SERVER_IP }} bash -c \"'docker-compose -f ssh-gateway/docker-compose.yml pull && docker-compose -f ssh-gateway/docker-compose.yml --env-file secrets/.env up -d'\"" 46 | -------------------------------------------------------------------------------- /.github/workflows/graphviz.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build Graphviz 4 | 5 | # Controls when the workflow will run 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | paths: 11 | - 'system.dot' 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v3 27 | - run: mkdir -p docs 28 | - run: cat system.dot | docker run --rm -i nshine/dot > docs/system-graph.png 29 | - uses: stefanzweifel/git-auto-commit-action@v4 30 | with: 31 | commit_message: Generate diagrams (graphviz) 32 | -------------------------------------------------------------------------------- /.github/workflows/upload-assets.yml: -------------------------------------------------------------------------------- 1 | name: Upload static assets 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'assets/' 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | upload: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repository 15 | uses: actions/checkout@master 16 | - uses: BetaHuhn/do-spaces-action@v2 17 | with: 18 | access_key: ${{ secrets.SPACES_ACCESS_KEY}} 19 | secret_key: ${{ secrets.SPACES_SECRET_KEY }} 20 | space_name: "cdn.rollycubes.com" 21 | space_region: "sfo3" 22 | cdn_domain: "cdn.rollycubes.com" 23 | source: assets 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | GameServer 2 | *.o 3 | *.a 4 | terminal/env 5 | *.pyc 6 | node_modules 7 | server_state.json 8 | .ccls-cache/ 9 | gcm.cache/ 10 | *.d 11 | **/.pre-shared-key 12 | auth/.twitch_secrets 13 | 14 | .setup-log.log 15 | 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "game/prometheus-cpp"] 2 | path = game/prometheus-cpp 3 | url = https://github.com/jupp0r/prometheus-cpp.git 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "array": "cpp", 4 | "atomic": "cpp", 5 | "bit": "cpp", 6 | "*.tcc": "cpp", 7 | "bitset": "cpp", 8 | "cctype": "cpp", 9 | "chrono": "cpp", 10 | "clocale": "cpp", 11 | "cmath": "cpp", 12 | "codecvt": "cpp", 13 | "condition_variable": "cpp", 14 | "cstdarg": "cpp", 15 | "cstddef": "cpp", 16 | "cstdint": "cpp", 17 | "cstdio": "cpp", 18 | "cstdlib": "cpp", 19 | "cstring": "cpp", 20 | "ctime": "cpp", 21 | "cwchar": "cpp", 22 | "cwctype": "cpp", 23 | "deque": "cpp", 24 | "forward_list": "cpp", 25 | "list": "cpp", 26 | "map": "cpp", 27 | "set": "cpp", 28 | "unordered_map": "cpp", 29 | "unordered_set": "cpp", 30 | "vector": "cpp", 31 | "exception": "cpp", 32 | "algorithm": "cpp", 33 | "functional": "cpp", 34 | "iterator": "cpp", 35 | "memory": "cpp", 36 | "memory_resource": "cpp", 37 | "numeric": "cpp", 38 | "optional": "cpp", 39 | "random": "cpp", 40 | "ratio": "cpp", 41 | "regex": "cpp", 42 | "string": "cpp", 43 | "string_view": "cpp", 44 | "system_error": "cpp", 45 | "tuple": "cpp", 46 | "type_traits": "cpp", 47 | "utility": "cpp", 48 | "fstream": "cpp", 49 | "initializer_list": "cpp", 50 | "iomanip": "cpp", 51 | "iosfwd": "cpp", 52 | "iostream": "cpp", 53 | "istream": "cpp", 54 | "limits": "cpp", 55 | "mutex": "cpp", 56 | "new": "cpp", 57 | "ostream": "cpp", 58 | "shared_mutex": "cpp", 59 | "sstream": "cpp", 60 | "stdexcept": "cpp", 61 | "streambuf": "cpp", 62 | "thread": "cpp", 63 | "cinttypes": "cpp", 64 | "typeinfo": "cpp", 65 | "valarray": "cpp" 66 | } 67 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sarah Schulte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Rolly Cubes

3 | 4 |

5 | 6 | Open in GitHub Codespaces 7 | 8 |
9 | 10 | 11 | 12 |

13 |

Rolling the dice since 2019.

14 |

A game of luck and... well, actually, it's mostly luck.

15 |
16 | 17 |

18 | Check out this project in production or in beta, 19 | and follow development on Twitch.tv! 20 |

21 | 22 |

23 | Want to contribute? 24 |
25 | [Developer Documentation] 26 |

27 | -------------------------------------------------------------------------------- /assets/achievements/3x_doubles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/3x_doubles.png -------------------------------------------------------------------------------- /assets/achievements/5x_doubles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/5x_doubles.png -------------------------------------------------------------------------------- /assets/achievements/7x_doubles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/7x_doubles.png -------------------------------------------------------------------------------- /assets/achievements/addicted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/addicted.png -------------------------------------------------------------------------------- /assets/achievements/astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/astronaut.png -------------------------------------------------------------------------------- /assets/achievements/beginners_luck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/beginners_luck.png -------------------------------------------------------------------------------- /assets/achievements/getting_started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/getting_started.png -------------------------------------------------------------------------------- /assets/achievements/good_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/good_job.png -------------------------------------------------------------------------------- /assets/achievements/negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/negative.png -------------------------------------------------------------------------------- /assets/achievements/oops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/oops.png -------------------------------------------------------------------------------- /assets/achievements/perfect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/perfect.png -------------------------------------------------------------------------------- /assets/achievements/rude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/rude.png -------------------------------------------------------------------------------- /assets/achievements/thief.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/achievements/thief.png -------------------------------------------------------------------------------- /assets/badges/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/badges/admin.png -------------------------------------------------------------------------------- /assets/badges/premium.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/assets/badges/premium.gif -------------------------------------------------------------------------------- /auth/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | -------------------------------------------------------------------------------- /auth/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | secrets/ 3 | db_backup 4 | -------------------------------------------------------------------------------- /auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auth" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | base64 = "0.21.2" 10 | ring = "0.16.20" 11 | hyper = "0.14.26" 12 | uuid = { version = "1.3.1", features = [ "serde", "v4" ] } 13 | tokio = { version = "1", features = ["full"] } 14 | tokio-postgres = { version = "0.7.8", features = [ "with-uuid-1", "with-chrono-0_4" ]} 15 | chrono = { version = "0.4.28", features = [ "serde", "std", "clock" ] } 16 | refinery = { version = "0.8", features = ["tokio-postgres"]} 17 | axum = { version = "0.6.16", features=[ "macros" ] } 18 | tracing = "0.1" 19 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 20 | serde = { version = "1.0.105", features = ["derive"] } 21 | serde_repr = "0.1" 22 | axum-extra = { version = "0.7.4", features = ["cookie"] } 23 | serde_json = "1.0.105" 24 | reqwest = { version = "0.11", features = ["json"] } 25 | openssl-sys = { version = "0.9.86", features = [ "vendored" ] } 26 | twitch_oauth2 = {version = "0.10", features = [ "reqwest" ] } 27 | twitch_api = {version="0.7.0-rc.4", features=["helix", "reqwest", "twitch_oauth2", "client"] } 28 | bb8-postgres = { version = "0.8.1", features = [ "with-uuid-1", "with-chrono-0_4" ]} 29 | bb8 = "0.8.0" 30 | jsonwebtoken = "8.3.0" 31 | tower-http = { version = "0.4.0", features = [ "cors", "catch-panic" ] } 32 | lazy_static = "1.4.0" 33 | tracing-log = "0.1.3" 34 | thiserror = "1.0" 35 | anyhow = "1.0" 36 | int-enum = "0.5" 37 | openssh-keys = "0.6.2" 38 | 39 | [build-dependencies] 40 | prettyplease = "0.2" 41 | schemars = "0.8" 42 | serde_json = "1.0.105" 43 | syn = "2.0.29" 44 | typify = "0.0.13" 45 | -------------------------------------------------------------------------------- /auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-alpine as planner 2 | RUN apk add musl-dev pkgconfig make perl 3 | 4 | WORKDIR /usr/build 5 | 6 | COPY . . 7 | RUN cargo chef prepare --recipe-path recipe.json 8 | 9 | FROM lukemathwalker/cargo-chef:latest-rust-alpine as builder 10 | RUN apk add musl-dev pkgconfig make perl 11 | 12 | WORKDIR /usr/build 13 | 14 | ENV SCHEMA_PATH . 15 | COPY --from=planner /usr/build/recipe.json recipe.json 16 | RUN cargo chef cook --recipe-path recipe.json --release 17 | 18 | COPY . . 19 | RUN cargo build --release 20 | 21 | FROM alpine:edge as prod 22 | 23 | COPY --from=builder /usr/build/target/release/auth . 24 | 25 | ENTRYPOINT ["./auth"] 26 | -------------------------------------------------------------------------------- /auth/badges.json: -------------------------------------------------------------------------------- 1 | { 2 | "premium": { 3 | "id": "premium", 4 | "name": "Premium", 5 | "description": "This user has upgraded to premium.", 6 | "image_url": "https://cdn.rollycubes.com/badges/premium.gif" 7 | }, 8 | "admin": { 9 | "id": "admin", 10 | "name": "Admin", 11 | "description": "This user helps run the website.", 12 | "image_url": "https://cdn.rollycubes.com/badges/admin.png" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /auth/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::{env, fs, path::Path}; 4 | 5 | use typify::{TypeSpace, TypeSpaceImpl, TypeSpaceSettings}; 6 | 7 | fn main() { 8 | println!("cargo:rerun-if-changed=../schema.json"); 9 | let mut ofile = Path::new(&env::var("SCHEMA_PATH").map_or_else(|_| "..".to_string(), |ok| ok)) 10 | .to_path_buf(); 11 | ofile.push("schema.json"); 12 | let content = std::fs::read_to_string(ofile).unwrap(); 13 | let schema = serde_json::from_str::(&content).unwrap(); 14 | 15 | let mut type_space = TypeSpace::new( 16 | TypeSpaceSettings::default() 17 | .with_struct_builder(true) 18 | .with_conversion( 19 | schemars::schema::SchemaObject { 20 | instance_type: Some(schemars::schema::InstanceType::String.into()), 21 | format: Some("date".to_string()), 22 | ..Default::default() 23 | }, 24 | "chrono::DateTime", 25 | [TypeSpaceImpl::Display].into_iter(), 26 | ), 27 | ); 28 | type_space.add_root_schema(schema).unwrap(); 29 | 30 | let contents = format!( 31 | "{}\n{}", 32 | "use serde::{Deserialize, Serialize};", 33 | prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()) 34 | ); 35 | 36 | let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); 37 | out_file.push("generated.rs"); 38 | fs::write(out_file, contents).unwrap(); 39 | } 40 | -------------------------------------------------------------------------------- /auth/devdb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: "postgres:9.6.1" 5 | ports: 6 | - '5432:5432' 7 | environment: 8 | POSTGRES_USER: "test" 9 | POSTGRES_PASSWORD: "test" 10 | POSTGRES_DB: "test" 11 | 12 | volumes: 13 | - postgres-data-auth:/var/lib/postgresql/data 14 | 15 | volumes: 16 | postgres-data-auth: 17 | -------------------------------------------------------------------------------- /auth/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | auth: 4 | image: ghcr.io/cgsdev0/rollycubes-auth 5 | restart: always 6 | environment: 7 | DB_HOST: "db" 8 | RUST_LOG: "debug" 9 | depends_on: 10 | - "postgres" 11 | links: 12 | - "postgres:db" 13 | volumes: 14 | - ../secrets/:/secrets 15 | postgres: 16 | image: "postgres:9.6.1" 17 | restart: always 18 | environment: 19 | POSTGRES_USER: "test" 20 | POSTGRES_PASSWORD: "test" 21 | POSTGRES_DB: "test" 22 | 23 | volumes: 24 | - postgres-data-auth:/var/lib/postgresql/data 25 | 26 | networks: 27 | default: 28 | name: rollycubes 29 | external: true 30 | 31 | volumes: 32 | postgres-data-auth: 33 | -------------------------------------------------------------------------------- /auth/migrations/V10__donors.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN donor boolean DEFAULT false NOT NULL; 3 | -------------------------------------------------------------------------------- /auth/migrations/V11__payments.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS payment ( 2 | user_id uuid NOT NULL, 3 | payment_id character varying NOT NULL 4 | ); 5 | 6 | ALTER TABLE ONLY payment 7 | ADD CONSTRAINT "FK_user_to_payment" FOREIGN KEY (user_id) REFERENCES users(id); 8 | -------------------------------------------------------------------------------- /auth/migrations/V12__pubkeys.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS pubkey_to_user ( 2 | pubkey character varying NOT NULL PRIMARY KEY, 3 | user_id uuid NOT NULL 4 | ); 5 | 6 | ALTER TABLE ONLY pubkey_to_user DROP CONSTRAINT IF EXISTS "FK_pubkey_to_userid"; 7 | ALTER TABLE ONLY pubkey_to_user 8 | ADD CONSTRAINT "FK_pubkey_to_userid" FOREIGN KEY (user_id) REFERENCES users(id); 9 | -------------------------------------------------------------------------------- /auth/migrations/V13__pubkey_settings.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_settings 2 | ADD COLUMN pubkey_text TEXT DEFAULT '' NOT NULL; 3 | -------------------------------------------------------------------------------- /auth/migrations/V14__dice_type_strings.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | ALTER TABLE user_settings ADD COLUMN dice_type_new VARCHAR(20) DEFAULT 'Default'; 3 | UPDATE user_settings SET dice_type_new = CASE WHEN dice_type = 0 THEN 'Default' WHEN dice_type = 1 THEN 'D20' WHEN dice_type = 2 THEN 'Golden' END; 4 | ALTER TABLE user_settings DROP COLUMN dice_type; 5 | ALTER TABLE user_settings RENAME COLUMN dice_type_new TO dice_type; 6 | COMMIT; 7 | -------------------------------------------------------------------------------- /auth/migrations/V15__histogram_tracking.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_stats 2 | ADD COLUMN dice_values integer[6] DEFAULT '{0,0,0,0,0,0}' NOT NULL, 3 | ADD COLUMN roll_totals integer[12] DEFAULT '{0,0,0,0,0,0,0,0,0,0,0,0}' NOT NULL; 4 | -------------------------------------------------------------------------------- /auth/migrations/V16__bigger_ints.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_stats 2 | ALTER COLUMN dice_values TYPE bigint[6], 3 | ALTER COLUMN dice_values SET DEFAULT '{0,0,0,0,0,0}', 4 | ALTER COLUMN rolls TYPE bigint, 5 | ALTER COLUMN doubles TYPE bigint, 6 | ALTER COLUMN wins TYPE bigint, 7 | ALTER COLUMN games TYPE bigint, 8 | ALTER COLUMN roll_totals TYPE bigint[12], 9 | ALTER COLUMN roll_totals SET DEFAULT '{0,0,0,0,0,0,0,0,0,0,0,0}'; 10 | -------------------------------------------------------------------------------- /auth/migrations/V17__winner_histograms.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_stats 2 | ADD COLUMN winning_scores bigint[6] DEFAULT '{0,0,0,0,0,0}' NOT NULL; 3 | -------------------------------------------------------------------------------- /auth/migrations/V2__drop_passwords.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" 2 | DROP COLUMN IF EXISTS hashed_password; 3 | -------------------------------------------------------------------------------- /auth/migrations/V3__snake_case_everything.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_stats RENAME COLUMN "userId" TO user_id; 2 | ALTER TABLE refresh_token RENAME COLUMN "userId" TO user_id; 3 | ALTER TABLE twitch_identity RENAME COLUMN "userId" TO user_id; 4 | ALTER TABLE user_to_achievement RENAME COLUMN "userId" TO user_id; 5 | ALTER TABLE user_to_achievement RENAME COLUMN "achievementId" TO achievement_id; 6 | ALTER TABLE public."user" RENAME TO users; 7 | ALTER TABLE users RENAME COLUMN "createdDate" TO created_date; 8 | -------------------------------------------------------------------------------- /auth/migrations/V4__drop_achievements.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ONLY user_to_achievement DROP CONSTRAINT IF EXISTS "FK_02102504ee06fb44948906ec7d6"; 2 | DROP TABLE IF EXISTS achievement; 3 | -------------------------------------------------------------------------------- /auth/migrations/V5__default_username.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ALTER COLUMN username SET DEFAULT 'Guest'; 2 | -------------------------------------------------------------------------------- /auth/migrations/V6__anon_identity.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS anon_identity ( 2 | anon_id character varying NOT NULL PRIMARY KEY, 3 | user_id uuid UNIQUE 4 | ); 5 | 6 | ALTER TABLE ONLY anon_identity 7 | ADD CONSTRAINT "FK_anon_identity_to_users" FOREIGN KEY (user_id) REFERENCES users(id); 8 | -------------------------------------------------------------------------------- /auth/migrations/V7__cascades.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ONLY twitch_identity DROP CONSTRAINT IF EXISTS "FK_096ac80126c38d3866baf1ba7a8"; 2 | ALTER TABLE ONLY twitch_identity 3 | ADD CONSTRAINT "FK_096ac80126c38d3866baf1ba7a8" FOREIGN KEY (user_id) REFERENCES users(id) 4 | ON DELETE CASCADE; 5 | 6 | ALTER TABLE ONLY user_to_achievement DROP CONSTRAINT IF EXISTS "FK_5d9a7b213a82555acbeb6d62bbe"; 7 | ALTER TABLE ONLY user_to_achievement 8 | ADD CONSTRAINT "FK_5d9a7b213a82555acbeb6d62bbe" FOREIGN KEY (user_id) REFERENCES users(id) 9 | ON DELETE CASCADE; 10 | 11 | ALTER TABLE ONLY refresh_token DROP CONSTRAINT IF EXISTS "FK_8e913e288156c133999341156ad"; 12 | ALTER TABLE ONLY refresh_token 13 | ADD CONSTRAINT "FK_8e913e288156c133999341156ad" FOREIGN KEY (user_id) REFERENCES users(id) 14 | ON DELETE CASCADE; 15 | 16 | ALTER TABLE ONLY player_stats DROP CONSTRAINT IF EXISTS "FK_a14e90bda5a40cf0b150c6dc87f"; 17 | ALTER TABLE ONLY player_stats 18 | ADD CONSTRAINT "FK_a14e90bda5a40cf0b150c6dc87f" FOREIGN KEY (user_id) REFERENCES users(id) 19 | ON DELETE CASCADE; 20 | 21 | ALTER TABLE ONLY anon_identity DROP CONSTRAINT IF EXISTS "FK_anon_identity_to_users"; 22 | ALTER TABLE ONLY anon_identity 23 | ADD CONSTRAINT "FK_anon_identity_to_users" FOREIGN KEY (user_id) REFERENCES users(id) 24 | ON DELETE CASCADE; 25 | -------------------------------------------------------------------------------- /auth/migrations/V8__settings.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS user_settings( 2 | user_id uuid NOT NULL, 3 | dice_type int DEFAULT 0, 4 | PRIMARY KEY(user_id), 5 | CONSTRAINT fk_user 6 | FOREIGN KEY(user_id) 7 | REFERENCES users(id) 8 | ); 9 | 10 | INSERT INTO user_settings(user_id) 11 | (SELECT id AS user_id FROM users); 12 | -------------------------------------------------------------------------------- /auth/migrations/V9__player_colors.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_settings 2 | ADD COLUMN color_hue double precision DEFAULT 0 NOT NULL, 3 | ADD COLUMN color_sat double precision DEFAULT 80 NOT NULL; 4 | -------------------------------------------------------------------------------- /auth/src/generated.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/generated.rs")); 2 | -------------------------------------------------------------------------------- /auth/src/migrations.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use tokio_postgres::Client; 3 | 4 | mod embedded { 5 | use refinery::embed_migrations; 6 | embed_migrations!("migrations"); 7 | } 8 | 9 | pub async fn run_migrations(client: &mut Client) -> std::result::Result<(), Error> { 10 | println!("Running DB migrations..."); 11 | let migration_report = embedded::migrations::runner().run_async(client).await?; 12 | 13 | for migration in migration_report.applied_migrations() { 14 | println!( 15 | "Migration Applied - Name: {}, Version: {}", 16 | migration.name(), 17 | migration.version() 18 | ); 19 | } 20 | 21 | println!("DB migrations finished!"); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /auth/tasks.toml: -------------------------------------------------------------------------------- 1 | [[task]] 2 | id = "run" 3 | type = "long" 4 | cmd = "cargo watch -x run" 5 | dependencies = [ "db" ] 6 | 7 | [[task]] 8 | id = "db" 9 | type = "short" 10 | cmd = "nc -zv localhost 5432 || (cd devdb; docker-compose up -d)" 11 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /client/functions/room/[room_id].ts: -------------------------------------------------------------------------------- 1 | export const onRequest: PagesFunction<{}> = async (context) => { 2 | const { request, env } = context; 3 | 4 | const res = await env.ASSETS.fetch(request); 5 | return new HTMLRewriter().on('meta', new MetaHandler()).transform(res); 6 | }; 7 | 8 | class MetaHandler implements HTMLRewriterElementContentHandlers { 9 | element(element: Element): void | Promise { 10 | if (element.getAttribute('name') === 'description') { 11 | element.setAttribute('content', 'Join my rolly cubes session!'); 12 | return; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "types": ["@cloudflare/workers-types"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/public/Icosphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/Icosphere.png -------------------------------------------------------------------------------- /client/public/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/crown.png -------------------------------------------------------------------------------- /client/public/default_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/default_player.png -------------------------------------------------------------------------------- /client/public/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/dice.png -------------------------------------------------------------------------------- /client/public/emissive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/emissive.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/gear.png -------------------------------------------------------------------------------- /client/public/gold6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/gold6.png -------------------------------------------------------------------------------- /client/public/hands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/hands.png -------------------------------------------------------------------------------- /client/public/itsboats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/itsboats.png -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dice Game", 3 | "name": "The Dice Game", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /client/public/normal6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/normal6.png -------------------------------------------------------------------------------- /client/public/normal6_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/normal6_2.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/public/skybox/room.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/room.hdr -------------------------------------------------------------------------------- /client/public/skybox/skybox_nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_nx.jpg -------------------------------------------------------------------------------- /client/public/skybox/skybox_ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_ny.jpg -------------------------------------------------------------------------------- /client/public/skybox/skybox_nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_nz.jpg -------------------------------------------------------------------------------- /client/public/skybox/skybox_px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_px.jpg -------------------------------------------------------------------------------- /client/public/skybox/skybox_py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_py.jpg -------------------------------------------------------------------------------- /client/public/skybox/skybox_pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgsdev0/rollycubes/4106af80fcc07634c20fd8c8851c1260d933d9f9/client/public/skybox/skybox_pz.jpg -------------------------------------------------------------------------------- /client/src/actions/settings.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from '@reduxjs/toolkit'; 2 | import { ReduxState } from 'store'; 3 | 4 | export function cheatsAction() { 5 | return Object.assign( 6 | (dispatch: Dispatch, getState: () => ReduxState) => { 7 | const cheats = getState().settings.cheats; 8 | 9 | const action = { type: 'CHEATS' as 'CHEATS', newState: !cheats }; 10 | dispatch(action); 11 | return action; 12 | }, 13 | { type: 'CHEATS_THUNK' } 14 | ); 15 | } 16 | 17 | export type CheatsAction = ReturnType>; 18 | -------------------------------------------------------------------------------- /client/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const TARGET_SCORES = [33, 66, 67, 98, 99, 100]; 2 | -------------------------------------------------------------------------------- /client/src/hooks/window_size.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | // Hook 3 | export function useWindowSize() { 4 | // Initialize state with undefined width/height so server and client renders match 5 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 6 | const [windowSize, setWindowSize] = useState<{ 7 | width?: number; 8 | height?: number; 9 | }>({ 10 | width: undefined, 11 | height: undefined, 12 | }); 13 | 14 | useEffect(() => { 15 | // Handler to call on window resize 16 | function handleResize() { 17 | // Set window width/height to state 18 | const size = { 19 | width: Math.min(window.screen.width, window.innerWidth), 20 | height: Math.min(window.screen.height, window.innerHeight), 21 | }; 22 | setWindowSize(size); 23 | } 24 | // Add event listener 25 | window.addEventListener('resize', handleResize); 26 | // Call handler right away so state gets updated with initial window size 27 | handleResize(); 28 | // Remove event listener on cleanup 29 | return () => window.removeEventListener('resize', handleResize); 30 | }, []); // Empty array ensures that effect is only run on mount 31 | return windowSize; 32 | } 33 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime'; 2 | import App from './App'; 3 | 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container!); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /client/src/pages/login.css: -------------------------------------------------------------------------------- 1 | div.loginForm { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .loginForm input { 7 | margin-bottom: 10px; 8 | height: 24px; 9 | font-size: 16pt; 10 | } 11 | .loginContainer button { 12 | width: 100%; 13 | margin: 0px; 14 | margin-bottom: 12px; 15 | padding: 6px; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /client/src/pages/tab_error_page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { css } from 'stitches.config'; 3 | 4 | const centered = css({ 5 | width: '100%', 6 | height: '100%', 7 | display: 'flex', 8 | justifyContent: 'center', 9 | alignItems: 'flex-end', 10 | }); 11 | 12 | interface Props { 13 | error: string; 14 | } 15 | export const GenericErrorPage: React.FC = ({ error }) => { 16 | return ( 17 |
18 |

Error

19 |

{error}

20 |
21 | Back to Home 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/providers/achievements.tsx: -------------------------------------------------------------------------------- 1 | import { useGetAchievementListQuery } from 'api/auth'; 2 | import React from 'react'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | export const AchievementProvider: React.FC<{}> = ({ children }) => { 6 | const { isLoading, data, error } = useGetAchievementListQuery(); 7 | const dispatch = useDispatch(); 8 | 9 | const preloaded = React.useRef([]); 10 | 11 | React.useEffect(() => { 12 | if (isLoading) return; 13 | if (error) { 14 | console.error(error); 15 | } else if (data) { 16 | dispatch({ type: 'GOT_ACHIEVEMENTS', achievements: data }); 17 | preloaded.current = Object.values(data) 18 | .map((a) => { 19 | if (!a.image_url) return null; 20 | const img = new Image(); 21 | img.src = a.image_url; 22 | return img; 23 | }) 24 | .filter(Boolean); 25 | } 26 | }, [isLoading, data, error, preloaded]); 27 | 28 | if (isLoading) return null; 29 | return <>{children}; 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/providers/auth.tsx: -------------------------------------------------------------------------------- 1 | import { useGetRefreshTokenQuery } from 'api/auth'; 2 | import React from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { ReduxState } from 'store'; 5 | 6 | export const AuthProvider: React.FC<{}> = ({ children }) => { 7 | const { isLoading, data, error, refetch } = useGetRefreshTokenQuery(); 8 | const dispatch = useDispatch(); 9 | const loaded = useSelector( 10 | (state) => state.auth.authToken !== undefined 11 | ); 12 | 13 | React.useEffect(() => { 14 | if (isLoading) return; 15 | if (error) { 16 | dispatch({ type: 'AUTHENTICATE', access_token: null }); 17 | } else if (data) { 18 | dispatch({ type: 'AUTHENTICATE', access_token: data.access_token }); 19 | // refetch the access token after 45 minutes 20 | const interval = setInterval(refetch, 1000 * 60 * 45); 21 | return () => clearInterval(interval); 22 | } 23 | }, [isLoading, data, error]); 24 | 25 | if (!loaded) return null; 26 | return <>{children}; 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/providers/badges.tsx: -------------------------------------------------------------------------------- 1 | import { useGetBadgeListQuery } from 'api/auth'; 2 | import React from 'react'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | export const BadgeProvider: React.FC<{}> = ({ children }) => { 6 | const { isLoading, data, error } = useGetBadgeListQuery(); 7 | const dispatch = useDispatch(); 8 | 9 | const preloaded = React.useRef([]); 10 | 11 | React.useEffect(() => { 12 | if (isLoading) return; 13 | if (error) { 14 | console.error(error); 15 | } else if (data) { 16 | dispatch({ type: 'GOT_BADGES', badges: data }); 17 | preloaded.current = Object.values(data) 18 | .map((a) => { 19 | if (!a.image_url) return null; 20 | const img = new Image(); 21 | img.src = a.image_url; 22 | return img; 23 | }) 24 | .filter(Boolean); 25 | } 26 | }, [isLoading, data, error, preloaded]); 27 | 28 | if (isLoading) return null; 29 | return <>{children}; 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'react-horizontal-scrolling'; 3 | -------------------------------------------------------------------------------- /client/src/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { AchievementList, BadgeList } from 'api/auth'; 3 | import decode from 'jwt-decode'; 4 | 5 | interface TokenUserData { 6 | exp: number; 7 | user_id: string; 8 | display_name: string; 9 | } 10 | 11 | export interface AuthState { 12 | authToken?: string | null; 13 | userData?: TokenUserData; 14 | achievements?: AchievementList; 15 | badges?: BadgeList; 16 | } 17 | 18 | export interface AuthenticateAction { 19 | type: 'AUTHENTICATE'; 20 | access_token: string; 21 | } 22 | 23 | export interface GotAchievementsAction { 24 | type: 'GOT_ACHIEVEMENTS'; 25 | achievements: AchievementList; 26 | } 27 | 28 | export interface GotBadgesAction { 29 | type: 'GOT_BADGES'; 30 | badges: BadgeList; 31 | } 32 | 33 | export const authReducer = createReducer({}, (builder) => { 34 | builder 35 | .addCase('GOT_ACHIEVEMENTS', (state, action: GotAchievementsAction) => { 36 | state.achievements = action.achievements; 37 | }) 38 | .addCase('GOT_BADGES', (state, action: GotBadgesAction) => { 39 | state.badges = action.badges; 40 | }) 41 | .addCase('AUTHENTICATE', (state, action: AuthenticateAction) => { 42 | try { 43 | const decoded = decode(action.access_token); 44 | state.authToken = action.access_token; 45 | state.userData = decoded; 46 | } catch (e) { 47 | state.authToken = null; 48 | state.userData = undefined; 49 | } 50 | }) 51 | .addCase('LOGOUT', (state, action) => { 52 | state.authToken = null; 53 | state.userData = undefined; 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /client/src/reducers/chat.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { CheatsAction } from 'actions/settings'; 3 | import { RichTextMsg, WelcomeMsg } from '../types/api'; 4 | 5 | export interface ChatState { 6 | chat: RichTextMsg[]; 7 | } 8 | const CHAT_BUFFER_LENGTH = 200; 9 | 10 | export const chatReducer = createReducer({ chat: [] }, (builder) => { 11 | builder 12 | .addCase('welcome', (state, action: WelcomeMsg) => { 13 | state.chat = action.richChatLog; 14 | }) 15 | .addCase('CHEATS', (state, action: CheatsAction) => { 16 | state.chat.unshift({ 17 | type: 'chat_v2', 18 | msg: ['Hints ' + (action.newState ? 'enabled.' : 'disabled.')], 19 | }); 20 | state.chat.length = Math.max(state.chat.length, CHAT_BUFFER_LENGTH); 21 | }) 22 | .addCase('chat_v2', (state, action: RichTextMsg) => { 23 | state.chat.unshift(action); 24 | state.chat.length = Math.max(state.chat.length, CHAT_BUFFER_LENGTH); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /client/src/reducers/connection.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | export interface ConnectionState { 4 | socket?: WebSocket; 5 | connected: boolean; 6 | } 7 | 8 | export interface WebsocketAction { 9 | type: 'WEBSOCKET'; 10 | socket?: WebSocket; 11 | } 12 | 13 | export const connectionReducer = createReducer( 14 | { connected: false }, 15 | (builder) => { 16 | builder 17 | .addCase('WEBSOCKET', (state, action: WebsocketAction) => { 18 | state.socket = action.socket; 19 | }) 20 | .addCase('socket_open', (state, action) => { 21 | state.connected = true; 22 | }) 23 | .addCase('socket_close', (state, action) => { 24 | state.connected = false; 25 | }); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /client/src/reducers/pop_text.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { UpdateMsg, UpdateTurnMsg } from '../types/api'; 3 | 4 | export interface PopTextState { 5 | rollCount: number; 6 | reset: boolean; 7 | popText: Array<{ text: string; color: string; id: number }>; 8 | has69: boolean; 9 | popTextId: number; 10 | } 11 | 12 | export const popTextReducer = createReducer( 13 | { 14 | rollCount: 0, 15 | reset: false, 16 | has69: false, 17 | popText: [], 18 | popTextId: 0, 19 | }, 20 | (builder) => { 21 | builder 22 | .addCase('DOUBLES', (state, action) => { 23 | state.popText.push({ 24 | text: 'Doubles!', 25 | color: 'cyan', 26 | id: state.popTextId++, 27 | }); 28 | }) 29 | .addCase('win', (state, action) => { 30 | state.popText.push({ 31 | text: '{winner} wins!', 32 | color: 'lime', 33 | id: state.popTextId++, 34 | }); 35 | }) 36 | .addCase('POP_NEXT', (state, action) => { 37 | state.popText.shift(); 38 | }) 39 | .addCase('update', (state, action: UpdateMsg) => { 40 | if (action.reset) { 41 | state.popText.push({ 42 | text: 'Reset!', 43 | color: 'red', 44 | id: state.popTextId++, 45 | }); 46 | } else { 47 | // ignore reset packets; they're irrelevant 48 | state.has69 = action.score === 69; 49 | } 50 | }) 51 | .addCase('update_turn', (state, action: UpdateTurnMsg) => { 52 | if (state.has69) { 53 | state.popText.push({ 54 | text: 'Nice.', 55 | color: 'yellow', 56 | id: state.popTextId++, 57 | }); 58 | state.has69 = false; 59 | } 60 | }) 61 | .addCase('roll', (state, action) => { 62 | state.rollCount++; 63 | }); 64 | } 65 | ); 66 | -------------------------------------------------------------------------------- /client/src/reducers/themes.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, isAllOf, isFulfilled } from '@reduxjs/toolkit'; 2 | import { endpoints } from 'api/auth'; 3 | import { createTheme } from 'stitches.config'; 4 | 5 | export interface ThemeState { 6 | themes: Record; 7 | } 8 | 9 | export const themesReducer = createReducer( 10 | { themes: {} }, 11 | (builder) => { 12 | builder.addCase('authApi/executeQuery/fulfilled', (state, action: any) => { 13 | if (endpoints.getUserById.matchFulfilled(action)) { 14 | if (action.payload.username !== 'badcop_') return; 15 | const { hue, sat } = action.payload.color; 16 | state.themes[action.payload.id] = createTheme({ 17 | colors: { 18 | bad: '#ff0000', 19 | primary: `hsl(${hue}, ${sat}, 90%)`, 20 | primaryDimmed: `hsl(${hue}, ${sat}, 60%)`, 21 | brand: `hsl(${hue}, ${sat}, 50%)`, 22 | brandFaded: `hsl(${hue}, ${sat}, 40%)`, 23 | gray400: `hsl(${hue}, ${sat}, 40%)`, 24 | gray500: `hsl(${hue}, ${sat}, 35%)`, 25 | gray600: `hsl(${hue}, ${sat}, 30%)`, 26 | gray700: `hsl(${hue}, ${sat}, 25%)`, 27 | gray750: `hsl(${hue}, ${sat}, 15%)`, 28 | gray800: `hsl(${hue}, ${sat}, 20%)`, 29 | gray900: `hsl(${hue}, ${sat}, 12%)`, 30 | }, 31 | }); 32 | } 33 | }); 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /client/src/textmods.css: -------------------------------------------------------------------------------- 1 | .-textmod-bold { 2 | font-weight: bold; 3 | } 4 | .-textmod-underline { 5 | text-decoration: underline; 6 | } 7 | .-textmod-italic { 8 | font-style: italic; 9 | } 10 | .-textmod-strikethrough { 11 | text-decoration: line-through; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/toastify.css: -------------------------------------------------------------------------------- 1 | #toastcontainer { 2 | --toastify-toast-width: 360px; 3 | --toastify-color-dark: var(--colors-gray700); 4 | --toastify-color-progress-dark: var(--colors-brand); 5 | } 6 | -------------------------------------------------------------------------------- /client/src/ui/achievement.css: -------------------------------------------------------------------------------- 1 | .achievement { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .achievement img { 7 | margin-right: 12px; 8 | } 9 | 10 | .achievement p { 11 | margin: 4px; 12 | } 13 | 14 | .achievement .header { 15 | color: #aaa; 16 | } 17 | 18 | .achievement .name { 19 | font-weight: bold; 20 | } 21 | -------------------------------------------------------------------------------- /client/src/ui/achievement.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'stitches.config'; 2 | import { AchievementUnlock } from 'types/api'; 3 | 4 | const AchievementDiv = styled('div', { 5 | display: 'flex', 6 | gap: 16, 7 | '& img': { 8 | borderRadius: 4, 9 | imageRendering: 'pixelated', 10 | }, 11 | }); 12 | 13 | export const Achievement = (props: AchievementUnlock) => { 14 | return ( 15 | 16 | {props.description} 22 |
23 |

Achievement Unlocked

24 |

{props.name}

25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css, styled } from 'stitches.config'; 3 | import defaultIcon from '/default_player.png'; 4 | import crownIcon from '/crown.png'; 5 | import { DisconnectedIcon } from './icons/disconnected'; 6 | 7 | interface Props { 8 | imageUrl?: string | null; 9 | n?: number; 10 | size?: number; 11 | crown?: boolean; 12 | disconnected?: boolean; 13 | isSignedIn?: boolean; 14 | } 15 | 16 | const avatarWrapper = (isSignedIn: boolean) => 17 | css({ 18 | alignItems: 'center', 19 | '@bp1': { 20 | marginRight: 8, 21 | }, 22 | position: 'relative', 23 | cursor: isSignedIn ? 'pointer' : 'inherit', 24 | '& .avatar': { 25 | borderRadius: '50%', 26 | }, 27 | })(); 28 | 29 | const disconnectedWrapper = css({ 30 | position: 'absolute', 31 | bottom: -5, 32 | zIndex: 5, 33 | left: -3, 34 | '@bp0': { 35 | bottom: 0, 36 | width: 24, 37 | height: 24, 38 | }, 39 | transform: 'scale(80%)', 40 | backgroundColor: '#000000aa', 41 | borderRadius: '50%', 42 | }); 43 | const Crown = styled('img', { 44 | position: 'absolute', 45 | top: -6, 46 | zIndex: 5, 47 | left: 6, 48 | }); 49 | 50 | const Avatar = React.forwardRef((props, ref) => { 51 | const { size, imageUrl, n, crown, disconnected, isSignedIn } = props; 52 | 53 | const forSureImageUrl = imageUrl || defaultIcon; 54 | 55 | const size2 = size || 36; 56 | 57 | return ( 58 | 59 | avatar 66 | {crown ? ( 67 | 68 | ) : null} 69 | {disconnected ? ( 70 | 71 | 72 | 73 | ) : null} 74 | 75 | ); 76 | }); 77 | 78 | export default Avatar; 79 | -------------------------------------------------------------------------------- /client/src/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { styled } from 'stitches.config'; 3 | import { ReduxState } from '../store'; 4 | import { usePopperTooltip } from 'react-popper-tooltip'; 5 | 6 | import React from 'react'; 7 | import { AchievementTooltip } from './player'; 8 | 9 | const BImg = styled('img', { 10 | imageRendering: 'pixelated', 11 | }); 12 | 13 | const HistogramTooltip = styled('div', { 14 | lineHeight: 'initial', 15 | fontSize: 16, 16 | }); 17 | 18 | export const BadgeImg = (props: { id: string; click?: boolean }) => { 19 | const [tooltipVisible, setTooltipVisible] = React.useState(false); 20 | const { getTooltipProps, setTooltipRef, setTriggerRef } = usePopperTooltip({ 21 | visible: tooltipVisible, 22 | onVisibleChange: setTooltipVisible, 23 | trigger: props.click ? 'click' : undefined, 24 | }); 25 | const badges = useSelector((state: ReduxState) => state.auth.badges) || {}; 26 | const badge = badges[props.id]; 27 | return ( 28 | <> 29 | {tooltipVisible ? ( 30 | 34 | 35 |
{badge?.name}
36 |

{badge?.description}

37 |
38 |
39 | ) : null} 40 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /client/src/ui/buttons/add_sub_button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getAddSubButtonClassSelector } from '../../selectors/game_selectors'; 4 | import { ReduxState } from '../../store'; 5 | import { Button } from './button'; 6 | import './buttons.css'; 7 | 8 | interface OwnProps { 9 | n?: number; 10 | } 11 | 12 | interface StateProps { 13 | socket?: WebSocket; 14 | addClass: string; 15 | subClass: string; 16 | } 17 | 18 | type Props = OwnProps & StateProps; 19 | 20 | const AddSubButton: React.FC = ({ socket, addClass, subClass, n }) => { 21 | const onClick = (a: string) => { 22 | if (socket) { 23 | if (n === undefined) { 24 | socket.send(JSON.stringify({ type: a })); 25 | } else { 26 | socket.send(JSON.stringify({ type: `${a}_nth`, n })); 27 | } 28 | } 29 | }; 30 | 31 | return ( 32 | 33 | 36 | 39 | 40 | ); 41 | }; 42 | 43 | const mapStateToProps = (state: ReduxState, ownProps: OwnProps): StateProps => { 44 | return { 45 | socket: state.connection.socket, 46 | addClass: getAddSubButtonClassSelector( 47 | typeof ownProps.n === 'number' ? ownProps.n + 1 : 'add' 48 | )(state), 49 | subClass: getAddSubButtonClassSelector( 50 | typeof ownProps.n === 'number' ? -(ownProps.n + 1) : 'sub' 51 | )(state), 52 | }; 53 | }; 54 | 55 | export default connect(mapStateToProps)(AddSubButton); 56 | -------------------------------------------------------------------------------- /client/src/ui/buttons/button.tsx: -------------------------------------------------------------------------------- 1 | import { customTheme, styled } from 'stitches.config'; 2 | 3 | export const Button = styled('button', { 4 | maxHeight: 48, 5 | height: 48, 6 | minHeight: 48, 7 | backgroundColor: '$gray400', 8 | textShadow: 'rgba(0, 0, 0, 0.1) 0px 1px 2px', 9 | boxShadow: 10 | 'rgba(255, 255, 255, 0.2) 0px 2px 1px 0px inset, rgba(0,0,0,0.3) 0px -5px 1px 0px inset, rgba(0, 0, 0, 0.15) 0px 6px 10px -3px', 11 | paddingBottom: 4, 12 | paddingLeft: 8, 13 | paddingRight: 8, 14 | '&:hover': { 15 | transition: 'unset', 16 | opacity: 0.8, 17 | }, 18 | '&:active': { 19 | marginTop: 3, 20 | maxHeight: 45, 21 | height: 45, 22 | minHeight: 45, 23 | boxShadow: 24 | 'rgba(255, 255, 255, 0.2) 0px 2px 1px 0px inset, rgba(0,0,0,0.3) 0px -1px 1px 0px inset, rgba(0, 0, 0, 0.15) 0px 6px 10px -3px', 25 | paddingBottom: 1, 26 | transition: 'unset', 27 | }, 28 | color: '$gray900', 29 | border: '1px solid rgba(0,0,0,0.3)', 30 | borderRadius: 8, 31 | fontSize: '24px', 32 | fontFamily: 'Montserrat', 33 | [`.${customTheme} &`]: { 34 | backgroundColor: '$brand', 35 | color: '$primary', 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /client/src/ui/buttons/buttons.css: -------------------------------------------------------------------------------- 1 | button { 2 | flex-basis: 100%; 3 | background-color: lightgray; 4 | border: 0; 5 | margin: 0px 5px; 6 | border-radius: 8px; 7 | font-family: 'Montserrat'; 8 | font-size: 20px; 9 | font-weight: bold; 10 | } 11 | 12 | button:focus { 13 | outline: 0; 14 | } 15 | 16 | .buttonColumn { 17 | display: flex; 18 | flex-direction: row; 19 | align-items: stretch; 20 | justify-content: space-between; 21 | flex-grow: 1; 22 | align-content: stretch; 23 | flex-basis: 100%; 24 | } 25 | 26 | .topButton { 27 | min-height: 40px; 28 | max-height: 40px; 29 | z-index: 40; 30 | } 31 | 32 | button.Add { 33 | background-color: #007bff !important; 34 | color: white; 35 | font-size: 18pt; 36 | font-weight: bold; 37 | } 38 | 39 | button.Subtract { 40 | background-color: #6c757d !important; 41 | color: white; 42 | font-size: 18pt; 43 | font-weight: bold; 44 | } 45 | 46 | button.Reset { 47 | background-color: #dc3545 !important; 48 | color: white; 49 | } 50 | 51 | button.Victory { 52 | background-color: #ffc107 !important; 53 | position: relative; 54 | overflow: hidden; 55 | } 56 | 57 | button.Victory::before { 58 | content: ''; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | z-index: 2; 63 | background: white; 64 | opacity: 0.3; 65 | height: 100%; 66 | width: 100%; 67 | background: linear-gradient(to right, white, white 70%, transparent 20px); 68 | transform-origin: left bottom; 69 | animation: shine 2s ease-in-out infinite; 70 | } 71 | 72 | @keyframes shine { 73 | 0% { 74 | transform: skewX(-45deg) translateX(-150%); 75 | } 76 | 80% { 77 | transform: skewX(-45deg) translateX(150%); 78 | } 79 | 100% { 80 | transform: skewX(-45deg) translateX(150%); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /client/src/ui/buttons/restart_button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { selectIsSpectator } from 'selectors/game_selectors'; 4 | import { ReduxState } from '../../store'; 5 | import { Button } from './button'; 6 | 7 | interface Props { 8 | socket?: WebSocket; 9 | isSpectator: boolean; 10 | } 11 | 12 | const RollButton: React.FC = ({ socket, isSpectator }) => { 13 | const onClick = () => { 14 | if (socket) { 15 | socket.send(JSON.stringify({ type: 'restart' })); 16 | } 17 | }; 18 | return isSpectator ? null : ; 19 | }; 20 | 21 | const mapStateToProps = (state: ReduxState) => { 22 | return { 23 | socket: state.connection.socket, 24 | isSpectator: selectIsSpectator(state), 25 | }; 26 | }; 27 | 28 | export default connect(mapStateToProps)(RollButton); 29 | -------------------------------------------------------------------------------- /client/src/ui/buttons/slider.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'stitches.config'; 2 | 3 | const SliderContainer = styled('div', { 4 | '& input': { 5 | appearance: 'none', 6 | background: '$gray500', 7 | height: 16, 8 | borderRadius: 16, 9 | display: 'flex', 10 | '&::-webkit-slider-thumb': { 11 | appearance: 'none', 12 | background: '$primary', 13 | width: 26, 14 | height: 26, 15 | borderRadius: '50%', 16 | }, 17 | }, 18 | }); 19 | 20 | const Fieldset = styled('fieldset', { 21 | border: 0, 22 | display: 'flex', 23 | alignItems: 'center', 24 | gap: 8, 25 | flex: 1, 26 | maxHeight: 34, 27 | transition: 'none', 28 | '& label': { 29 | color: '$primary', 30 | display: 'flex', 31 | }, 32 | '&:has(:focus-visible)': { 33 | border: '2px solid $primary', 34 | margin: -2, 35 | }, 36 | }); 37 | 38 | interface Props { 39 | id: string; 40 | desc: string; 41 | min: number; 42 | max: number; 43 | value: number; 44 | onChange: React.ChangeEventHandler; 45 | } 46 | 47 | export const Slider: React.FC = ({ 48 | id, 49 | desc, 50 | min, 51 | max, 52 | value, 53 | onChange, 54 | }) => { 55 | return ( 56 |
57 | 58 | 66 | 67 | 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /client/src/ui/buttons/textarea.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'stitches.config'; 2 | 3 | const Fieldset = styled('fieldset', { 4 | border: 0, 5 | display: 'flex', 6 | flexDirection: 'column', 7 | gap: 8, 8 | flex: 1, 9 | transition: 'none', 10 | '& label': { 11 | color: '$primary', 12 | }, 13 | '&:has(:focus-visible)': { 14 | border: '2px solid $primary', 15 | margin: -2, 16 | }, 17 | }); 18 | 19 | const Textarea = styled('textarea', { 20 | flex: 1, 21 | backgroundColor: '$gray900', 22 | '@bp0': { 23 | backgroundColor: '$gray800', 24 | }, 25 | border: 0, 26 | resize: 'none', 27 | fontFamily: 'Amiko', 28 | padding: 8, 29 | borderRadius: 8, 30 | color: '$primaryDimmed', 31 | '&:focus-visible': { 32 | color: '$primary', 33 | }, 34 | }); 35 | 36 | interface Props { 37 | id: string; 38 | desc: string; 39 | value: string; 40 | onChange: React.ChangeEventHandler; 41 | placeholder?: string; 42 | } 43 | 44 | export const TextArea: React.FC = ({ 45 | placeholder, 46 | id, 47 | desc, 48 | value, 49 | onChange, 50 | }) => { 51 | return ( 52 |
53 | 54 |