├── backend
├── ponk
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_delete_bonkevent_remove_user_bonk_token.py
│ │ └── 0003_alter_image_image_alter_user_avatar.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── setting.py
│ ├── admin.py
│ ├── wsgi.py
│ ├── utils.py
│ ├── asgi.py
│ ├── api_decorators.py
│ ├── debug.py
│ ├── urls.py
│ ├── bonk.py
│ ├── ftapi.py
│ ├── models.py
│ ├── end.py
│ ├── money.py
│ ├── chat.py
│ ├── private.py
│ ├── skins.json
│ ├── friends.py
│ ├── auth.py
│ └── settings.py
├── pong-server
│ ├── Makefile
│ └── Cargo.toml
├── bonk-server
│ ├── asset
│ │ ├── circle_full.png
│ │ ├── Ubuntu-Regular.ttf
│ │ ├── circle.svg
│ │ ├── Ubuntu-Regular.ttf.import
│ │ ├── icon.svg
│ │ ├── circle_full.png.import
│ │ ├── city.svg.import
│ │ ├── icon.svg.import
│ │ ├── circle.svg.import
│ │ └── city.svg
│ ├── map
│ │ ├── rectangle.gd
│ │ ├── collision.gd
│ │ ├── map.gd
│ │ └── map.tscn
│ ├── player
│ │ ├── grapple.gd
│ │ └── player.tscn
│ ├── class
│ │ ├── User.gd
│ │ └── Room.gd
│ ├── settings.gd
│ ├── global.gd
│ ├── main.tscn
│ ├── export_presets.cfg
│ └── project.godot
├── media
│ └── images
│ │ └── victorosaurusrex.jpg
└── manage.py
├── startup.d
├── 15-stop
├── 05-pnpm
├── 02-source_env
├── 10-frontend_static_build
├── 11-husky
├── 09-debug
├── 01-private_api_token
├── 07-auto_migrate
├── 13-django
├── 03-env
├── 06-venv
├── 14-tmux
├── 04-init_db
├── 08-caddy
└── 12-bonk
├── .lintstagedrc
├── .set_host_ip_address
├── .dockerignore
├── assets
├── sheldon.png
├── skins
│ ├── blue.png
│ ├── cash.png
│ ├── data.png
│ ├── dinos.png
│ ├── green.png
│ ├── louis.png
│ ├── orcky.png
│ ├── red.png
│ ├── vader.png
│ ├── alex-1.png
│ ├── alex-2.png
│ ├── alex-3.png
│ ├── alexa-1.png
│ ├── alexa-2.png
│ ├── alexa-3.png
│ ├── cgodard.png
│ ├── dodo-1.png
│ ├── dodo-2.png
│ ├── dodo-3.png
│ ├── jcario.png
│ ├── kaubry.png
│ ├── kylian.png
│ ├── laubry.png
│ ├── mborde.png
│ ├── poulet1.png
│ ├── poulet2.png
│ ├── poulet3.png
│ ├── aboyreau.png
│ ├── acasamit.png
│ ├── atellier.png
│ ├── charchar-1.png
│ ├── charchar-2.png
│ ├── charchar-3.png
│ ├── chat-chen.png
│ ├── chat-jesus.png
│ ├── chat-nils.png
│ ├── chat-yanne.png
│ ├── cheveux-1.png
│ ├── cheveux-2.png
│ ├── cheveux-3.png
│ ├── dino-chat.png
│ ├── gprigent.png
│ ├── gwen-crous.png
│ ├── insu-anan.png
│ ├── insu-juju.png
│ ├── juju-crous.png
│ ├── lgalloux.png
│ ├── loustic-3.png
│ ├── maxborde.png
│ ├── mbordeau.png
│ ├── ndavenne.png
│ ├── nils-ninja.png
│ ├── nlaerema.png
│ ├── rriviere.png
│ ├── rrouille.png
│ ├── ryan-ninja.png
│ ├── tom-crous.png
│ ├── vcornill.png
│ ├── autre-dinos.png
│ ├── autre-louis.png
│ ├── autre-orcky.png
│ ├── chat-antoine.png
│ ├── encore-dinos.png
│ ├── encore-orcky.png
│ ├── insu-gwigwi.png
│ ├── pote-antoine.png
│ ├── ze_ouizarde.png
│ ├── monstre-colere.png
│ ├── monstre-heureux.png
│ ├── monstre-triste.png
│ ├── ninja-effrayant.png
│ ├── loustic-principal.png
│ ├── autre-pote-antoine.png
│ ├── chayanne-from-emma.png
│ ├── encore-pote-antoine.png
│ ├── pointing-finger-at-you.png
│ ├── louistic-2-mais-quand-meme-avec-le-loustic-principal.png
│ └── convert_image.sh
└── d1c625cd0008f4d8a8e9574d1c70c3b8.gif
├── frontend
├── pong-client
│ ├── .gitignore
│ ├── package.json
│ ├── .appveyor.yml
│ ├── src
│ │ └── game.rs
│ ├── Cargo.toml
│ └── .travis.yml
├── postcss.config.js
├── src
│ ├── Contexts.js
│ ├── pages
│ │ ├── _404.jsx
│ │ ├── Tournament
│ │ │ ├── Waiting.jsx
│ │ │ └── Menu.jsx
│ │ ├── LegalNotice
│ │ │ └── LegalNotice.jsx
│ │ ├── Games
│ │ │ ├── bonk.css
│ │ │ ├── Pong.jsx
│ │ │ └── Bonk.jsx
│ │ ├── Play
│ │ │ └── index.jsx
│ │ ├── Auth
│ │ │ ├── Signup.jsx
│ │ │ └── Login.jsx
│ │ ├── Shop
│ │ │ └── index.jsx
│ │ └── Profile
│ │ │ └── index.jsx
│ ├── components
│ │ ├── utils
│ │ │ ├── Card.jsx
│ │ │ ├── Input.jsx
│ │ │ ├── CTA.jsx
│ │ │ └── PopUp.jsx
│ │ ├── Shop
│ │ │ ├── Price.jsx
│ │ │ ├── BallPresentation.jsx
│ │ │ ├── ShopItem.jsx
│ │ │ └── ItemSelection.jsx
│ │ ├── Profile
│ │ │ ├── Level.jsx
│ │ │ ├── GameCard.jsx
│ │ │ ├── GameHistory.jsx
│ │ │ ├── Stat.jsx
│ │ │ ├── FriendCard.jsx
│ │ │ └── FriendList.jsx
│ │ ├── Navbar
│ │ │ └── LogCard.jsx
│ │ ├── Chat
│ │ │ ├── Message.jsx
│ │ │ └── Chat.jsx
│ │ └── Tournament
│ │ │ ├── InvitationCard.jsx
│ │ │ └── PlayerCard.jsx
│ ├── style.css
│ ├── index.jsx
│ └── scripts
│ │ └── languages.js
├── tailwind.config.js
├── .gitignore
├── jsconfig.json
├── vite.config.js
├── package.json
└── index.html
├── docker
├── pong-server
├── transcendence
└── bonk-server
├── .env.example
├── .husky
├── commit-msg
└── pre-commit
├── package.json
├── .gitignore
├── docker-compose.yml
├── Makefile
├── requirements.txt
├── README.md
└── shell.nix
/backend/ponk/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/ponk/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/ponk/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/startup.d/15-stop:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | caddy stop
4 | exec pg_ctl stop
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,jsx}": "prettier --write",
3 | "*.py": "black",
4 | }
5 |
--------------------------------------------------------------------------------
/.set_host_ip_address:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export HOST_IP_ADDRESS=`hostname -I | cut -d\ -f1`
4 |
--------------------------------------------------------------------------------
/startup.d/05-pnpm:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | yes | pnpm install
4 | yes | pnpm install -C frontend
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .initial_migration_done
3 | backend/.pg
4 | backend/media
5 | venv
6 |
--------------------------------------------------------------------------------
/backend/pong-server/Makefile:
--------------------------------------------------------------------------------
1 | pong-server:
2 | cargo run --release
3 |
4 | .PHONY: pong-server
5 |
--------------------------------------------------------------------------------
/startup.d/02-source_env:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -o allexport
4 | source .env
5 | set +o allexport
6 |
--------------------------------------------------------------------------------
/assets/sheldon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/sheldon.png
--------------------------------------------------------------------------------
/assets/skins/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/blue.png
--------------------------------------------------------------------------------
/assets/skins/cash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/cash.png
--------------------------------------------------------------------------------
/assets/skins/data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/data.png
--------------------------------------------------------------------------------
/assets/skins/dinos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/dinos.png
--------------------------------------------------------------------------------
/assets/skins/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/green.png
--------------------------------------------------------------------------------
/assets/skins/louis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/louis.png
--------------------------------------------------------------------------------
/assets/skins/orcky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/orcky.png
--------------------------------------------------------------------------------
/assets/skins/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/red.png
--------------------------------------------------------------------------------
/assets/skins/vader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/vader.png
--------------------------------------------------------------------------------
/frontend/pong-client/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 | bin/
5 | pkg/
6 | wasm-pack.log
7 |
--------------------------------------------------------------------------------
/assets/skins/alex-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alex-1.png
--------------------------------------------------------------------------------
/assets/skins/alex-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alex-2.png
--------------------------------------------------------------------------------
/assets/skins/alex-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alex-3.png
--------------------------------------------------------------------------------
/assets/skins/alexa-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alexa-1.png
--------------------------------------------------------------------------------
/assets/skins/alexa-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alexa-2.png
--------------------------------------------------------------------------------
/assets/skins/alexa-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/alexa-3.png
--------------------------------------------------------------------------------
/assets/skins/cgodard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/cgodard.png
--------------------------------------------------------------------------------
/assets/skins/dodo-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/dodo-1.png
--------------------------------------------------------------------------------
/assets/skins/dodo-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/dodo-2.png
--------------------------------------------------------------------------------
/assets/skins/dodo-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/dodo-3.png
--------------------------------------------------------------------------------
/assets/skins/jcario.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/jcario.png
--------------------------------------------------------------------------------
/assets/skins/kaubry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/kaubry.png
--------------------------------------------------------------------------------
/assets/skins/kylian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/kylian.png
--------------------------------------------------------------------------------
/assets/skins/laubry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/laubry.png
--------------------------------------------------------------------------------
/assets/skins/mborde.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/mborde.png
--------------------------------------------------------------------------------
/assets/skins/poulet1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/poulet1.png
--------------------------------------------------------------------------------
/assets/skins/poulet2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/poulet2.png
--------------------------------------------------------------------------------
/assets/skins/poulet3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/poulet3.png
--------------------------------------------------------------------------------
/assets/skins/aboyreau.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/aboyreau.png
--------------------------------------------------------------------------------
/assets/skins/acasamit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/acasamit.png
--------------------------------------------------------------------------------
/assets/skins/atellier.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/atellier.png
--------------------------------------------------------------------------------
/assets/skins/charchar-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/charchar-1.png
--------------------------------------------------------------------------------
/assets/skins/charchar-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/charchar-2.png
--------------------------------------------------------------------------------
/assets/skins/charchar-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/charchar-3.png
--------------------------------------------------------------------------------
/assets/skins/chat-chen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chat-chen.png
--------------------------------------------------------------------------------
/assets/skins/chat-jesus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chat-jesus.png
--------------------------------------------------------------------------------
/assets/skins/chat-nils.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chat-nils.png
--------------------------------------------------------------------------------
/assets/skins/chat-yanne.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chat-yanne.png
--------------------------------------------------------------------------------
/assets/skins/cheveux-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/cheveux-1.png
--------------------------------------------------------------------------------
/assets/skins/cheveux-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/cheveux-2.png
--------------------------------------------------------------------------------
/assets/skins/cheveux-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/cheveux-3.png
--------------------------------------------------------------------------------
/assets/skins/dino-chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/dino-chat.png
--------------------------------------------------------------------------------
/assets/skins/gprigent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/gprigent.png
--------------------------------------------------------------------------------
/assets/skins/gwen-crous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/gwen-crous.png
--------------------------------------------------------------------------------
/assets/skins/insu-anan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/insu-anan.png
--------------------------------------------------------------------------------
/assets/skins/insu-juju.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/insu-juju.png
--------------------------------------------------------------------------------
/assets/skins/juju-crous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/juju-crous.png
--------------------------------------------------------------------------------
/assets/skins/lgalloux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/lgalloux.png
--------------------------------------------------------------------------------
/assets/skins/loustic-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/loustic-3.png
--------------------------------------------------------------------------------
/assets/skins/maxborde.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/maxborde.png
--------------------------------------------------------------------------------
/assets/skins/mbordeau.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/mbordeau.png
--------------------------------------------------------------------------------
/assets/skins/ndavenne.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/ndavenne.png
--------------------------------------------------------------------------------
/assets/skins/nils-ninja.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/nils-ninja.png
--------------------------------------------------------------------------------
/assets/skins/nlaerema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/nlaerema.png
--------------------------------------------------------------------------------
/assets/skins/rriviere.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/rriviere.png
--------------------------------------------------------------------------------
/assets/skins/rrouille.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/rrouille.png
--------------------------------------------------------------------------------
/assets/skins/ryan-ninja.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/ryan-ninja.png
--------------------------------------------------------------------------------
/assets/skins/tom-crous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/tom-crous.png
--------------------------------------------------------------------------------
/assets/skins/vcornill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/vcornill.png
--------------------------------------------------------------------------------
/assets/skins/autre-dinos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/autre-dinos.png
--------------------------------------------------------------------------------
/assets/skins/autre-louis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/autre-louis.png
--------------------------------------------------------------------------------
/assets/skins/autre-orcky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/autre-orcky.png
--------------------------------------------------------------------------------
/assets/skins/chat-antoine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chat-antoine.png
--------------------------------------------------------------------------------
/assets/skins/encore-dinos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/encore-dinos.png
--------------------------------------------------------------------------------
/assets/skins/encore-orcky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/encore-orcky.png
--------------------------------------------------------------------------------
/assets/skins/insu-gwigwi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/insu-gwigwi.png
--------------------------------------------------------------------------------
/assets/skins/pote-antoine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/pote-antoine.png
--------------------------------------------------------------------------------
/assets/skins/ze_ouizarde.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/ze_ouizarde.png
--------------------------------------------------------------------------------
/assets/skins/monstre-colere.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/monstre-colere.png
--------------------------------------------------------------------------------
/assets/skins/monstre-heureux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/monstre-heureux.png
--------------------------------------------------------------------------------
/assets/skins/monstre-triste.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/monstre-triste.png
--------------------------------------------------------------------------------
/assets/skins/ninja-effrayant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/ninja-effrayant.png
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/assets/skins/loustic-principal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/loustic-principal.png
--------------------------------------------------------------------------------
/assets/skins/autre-pote-antoine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/autre-pote-antoine.png
--------------------------------------------------------------------------------
/assets/skins/chayanne-from-emma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/chayanne-from-emma.png
--------------------------------------------------------------------------------
/assets/skins/encore-pote-antoine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/encore-pote-antoine.png
--------------------------------------------------------------------------------
/assets/skins/pointing-finger-at-you.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/pointing-finger-at-you.png
--------------------------------------------------------------------------------
/startup.d/10-frontend_static_build:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$prod" = true ]; then
4 | pnpm -C frontend wasm
5 | pnpm -C frontend build
6 | fi
7 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/circle_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/backend/bonk-server/asset/circle_full.png
--------------------------------------------------------------------------------
/backend/media/images/victorosaurusrex.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/backend/media/images/victorosaurusrex.jpg
--------------------------------------------------------------------------------
/startup.d/11-husky:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$prod" = false ]; then
4 | # install git commit hooks
5 | pnpx husky install >/dev/null 2>&1
6 | fi
7 |
--------------------------------------------------------------------------------
/assets/d1c625cd0008f4d8a8e9574d1c70c3b8.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/d1c625cd0008f4d8a8e9574d1c70c3b8.gif
--------------------------------------------------------------------------------
/backend/bonk-server/asset/Ubuntu-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/backend/bonk-server/asset/Ubuntu-Regular.ttf
--------------------------------------------------------------------------------
/startup.d/09-debug:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | if [ "$prod" = true ]; then
6 | export FT_DEBUG=n
7 | else
8 | export FT_DEBUG=y
9 | fi
10 |
--------------------------------------------------------------------------------
/startup.d/01-private_api_token:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if ! grep -q PRIVATE_API_TOKEN .env; then
4 | echo PRIVATE_API_TOKEN=`openssl rand 16 | base64` >> .env
5 | fi
6 |
--------------------------------------------------------------------------------
/startup.d/07-auto_migrate:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ ! -e .initial_migration_done ]; then
4 | python3 backend/manage.py migrate
5 | touch .initial_migration_done
6 | fi
7 |
--------------------------------------------------------------------------------
/docker/pong-server:
--------------------------------------------------------------------------------
1 | FROM nixos/nix
2 |
3 | COPY . /app
4 |
5 | WORKDIR /app
6 |
7 | CMD ["nix-shell", "--arg", "prod", "true", "--argstr", "should-start", "pong-server"]
8 |
--------------------------------------------------------------------------------
/frontend/src/Contexts.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "preact";
2 |
3 | export const ProfileContext = createContext(null);
4 | export const LangContext = createContext("en");
5 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | CLIENT_ID=...
2 | CLIENT_SECRET=...
3 | DB_NAME=ft_transcendence
4 | DB_PASS=ft_transcendence
5 | DB_USER=ft_transcendence
6 | VITE_STRIPE_API_KEY=...
7 | STRIPE_WEBHOOK_KEY=...
--------------------------------------------------------------------------------
/assets/skins/louistic-2-mais-quand-meme-avec-le-loustic-principal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bonk-Corporation/ft_transcendence/HEAD/assets/skins/louistic-2-mais-quand-meme-avec-le-loustic-principal.png
--------------------------------------------------------------------------------
/backend/bonk-server/map/rectangle.gd:
--------------------------------------------------------------------------------
1 | extends Node2D
2 |
3 | func _ready():
4 | queue_redraw()
5 |
6 | func _draw():
7 | draw_rect(Rect2(Vector2.ZERO, Vector2(2000, 5000)), Color(0.05, 0.11, 0.16, 1))
8 |
--------------------------------------------------------------------------------
/startup.d/13-django:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | if [ "$prod" = true ]; then
6 | ( sleep 3 && echo -e \
7 | "\x1b[2J\x1b[1mListening on https://localhost:8443/\x1b[0m" ) &
8 | make
9 | fi
10 |
--------------------------------------------------------------------------------
/backend/bonk-server/player/grapple.gd:
--------------------------------------------------------------------------------
1 | extends Line2D
2 |
3 | func _process(_delta):
4 | queue_redraw()
5 |
6 | func _draw():
7 | if is_visible():
8 | draw_circle(get_point_position(1), 4, Color(1, 1, 1, 0.5))
9 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.jsx"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/assets/skins/convert_image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | image=${1?Missing image}
4 | convert \
5 | -resize 512x512\^ \
6 | -gravity Center \
7 | -extent 512x512 \
8 | -format png \
9 | "$image" "${image%%.*}.png"
10 |
--------------------------------------------------------------------------------
/docker/transcendence:
--------------------------------------------------------------------------------
1 | FROM nixos/nix
2 |
3 | RUN echo "demostanis:x:1000:1000:demostanis::/sbin/nologin" >> /etc/passwd
4 |
5 | COPY . /app
6 |
7 | WORKDIR /app
8 |
9 | CMD ["nix-shell", "--arg", "prod", "true"]
10 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | header=`cat "$1" | cut -c1-5 | head -1`
2 |
3 | exec bash <<- EOF
4 | if [[ ! "$header" = [+\-~]" | "[A-Z] ]]; then
5 | echo Invalid commit message, check the README.md for guidelines >&2
6 | exit 1
7 | fi
8 | EOF
9 |
10 | # vim:set ft=sh:
11 |
--------------------------------------------------------------------------------
/backend/bonk-server/class/User.gd:
--------------------------------------------------------------------------------
1 | class_name User
2 |
3 | var username: String
4 | var skin: String
5 | var id: int
6 | var joined_room: bool = false
7 | func _init(username: String, skin: String, id: int):
8 | self.username = username
9 | self.skin = skin
10 | self.id = id
11 |
12 |
--------------------------------------------------------------------------------
/docker/bonk-server:
--------------------------------------------------------------------------------
1 | FROM nixos/nix
2 |
3 | RUN mkdir /lib
4 | RUN ln -sf /lib /lib64
5 | RUN ln -sf "$(echo /nix/store/*/bin/*ld*.so | head -1)" /lib/ld-linux-x86-64.so.2
6 |
7 | COPY . /app
8 |
9 | WORKDIR /app
10 |
11 | CMD ["nix-shell", "--arg", "prod", "true", "--argstr", "should-start", "bonk-server"]
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "autoprefixer": "^10.4.19",
4 | "husky": "^9.0.11",
5 | "lint-staged": "^15.2.2",
6 | "postcss": "^8.4.38",
7 | "tailwindcss": "^3.4.3"
8 | },
9 | "dependencies": {
10 | "chart.js": "^4.4.2"
11 | },
12 | "scripts": {
13 | "prepare": "husky"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | db.sqlite3
3 | *.sw*
4 | __pycache__
5 | node_modules
6 | venv
7 | .env
8 | .pnpm-store
9 | .initial_migration_done
10 | .pg
11 | pg.log
12 | target
13 | pkg
14 | .static
15 | assets/avatars
16 | frontend/public
17 | backend/bonk-server/pkg
18 | backend/bonk-server/.godot
19 | frontend/bonk-client
20 |
--------------------------------------------------------------------------------
/startup.d/03-env:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export SHELL=zsh
4 | export PS1="[1mBonk Corporation[0m %% "
5 | export "PATH=venv/bin/:$PATH"
6 | export PIP_DISABLE_PIP_VERSION_CHECK=1
7 | export NIX_IGNORE_SYMLINK_STORE=1
8 | export NIX_ENFORCE_PURITY=0
9 | export PGDATA="backend/.pg"
10 | export UV_USE_IO_URING=0
11 | export TERM=xterm
12 |
--------------------------------------------------------------------------------
/backend/bonk-server/map/collision.gd:
--------------------------------------------------------------------------------
1 | extends CollisionShape2D
2 |
3 | func _draw():
4 | var rect_shape = shape as RectangleShape2D;
5 | var color = Color(0.4, 0.4, 0.4, 1)
6 | draw_rect(Rect2(-rect_shape.extents, rect_shape.extents * 2.0), color)
7 | draw_rect(Rect2(-rect_shape.extents, rect_shape.extents * 2.0), Color(0.33, 0.33, 0.33, 1), false, 3)
8 |
9 |
--------------------------------------------------------------------------------
/backend/ponk/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3 | from ponk.models import User
4 |
5 |
6 | @admin.register(User)
7 | class UserAdmin(BaseUserAdmin):
8 | list_display = ["username"]
9 |
10 |
11 | admin.site.unregister(User)
12 | admin.site.register(User, UserAdmin)
13 |
--------------------------------------------------------------------------------
/startup.d/06-venv:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | make venv
6 |
7 | if [ "$prod" = true ]; then
8 | # in docker
9 | sed -i '1s|.*|#!/app/venv/bin/python3|' venv/bin/pip3
10 | else
11 | sed -i "1s|.*|#!$PWD/venv/bin/python3|" venv/bin/pip3
12 | fi
13 |
14 | source venv/bin/activate
15 |
16 | pip3 install -r requirements.txt --break-system-packages
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/_404.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "preact/hooks";
2 | import { LangContext } from "../Contexts";
3 | import { language } from "../scripts/languages";
4 |
5 | export function NotFound({ lang }) {
6 | return (
7 |
8 | 404: Not Found
9 | {language.not_found[lang]}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/backend/bonk-server/class/Room.gd:
--------------------------------------------------------------------------------
1 | class_name Room
2 |
3 | var name: String
4 | var layer: int
5 | var users: Array
6 |
7 | func _init(name: String, layer: int):
8 | self.name = name
9 | self.layer = layer
10 | self.users = []
11 |
12 | func add_user(user: User):
13 | self.users.append(user)
14 |
15 | func remove_user(user: User):
16 | self.users.erase(user)
17 |
--------------------------------------------------------------------------------
/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 | build
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/frontend/pong-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "build": "webpack",
4 | "serve": "webpack serve"
5 | },
6 | "devDependencies": {
7 | "@wasm-tool/wasm-pack-plugin": "1.5.0",
8 | "html-webpack-plugin": "^5.3.2",
9 | "text-encoding": "^0.7.0",
10 | "webpack": "^5.49.0",
11 | "webpack-cli": "^4.7.2",
12 | "webpack-dev-server": "^4.15.1"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | if ! which prettier >/dev/null 2>&1; then
2 | echo Please install Prettier to ensure your JS files are properly formatted >&2
3 | exit 1
4 | fi
5 |
6 | if ! which black >/dev/null 2>&1; then
7 | echo Please install Black to ensure your Python files are properly formatted >&2
8 | exit 1
9 | fi
10 |
11 | ./node_modules/lint-staged/bin/lint-staged.js
12 |
13 | # vim:set ft=sh:
14 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function Card({ className = "", children, ...rest }) {
4 | return (
5 |
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 |
3 | export const Input = forwardRef((props, ref) => {
4 | return (
5 |
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/frontend/pong-client/.appveyor.yml:
--------------------------------------------------------------------------------
1 | install:
2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
5 | - rustc -V
6 | - cargo -V
7 |
8 | build: false
9 |
10 | test_script:
11 | - cargo test --locked
12 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/CTA.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function CTA({className = "", transparent=false, children, ...props}) {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/components/Shop/Price.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function Price({ price }) {
4 | return (
5 |
6 |
{price}
7 |
8 | B
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/backend/ponk/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for ponk project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ponk.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "noEmit": true,
7 | "allowJs": true,
8 | "checkJs": true,
9 |
10 | /* Preact Config */
11 | "jsx": "react-jsx",
12 | "jsxImportSource": "preact",
13 | "skipLibCheck": true,
14 | "paths": {
15 | "react": ["./node_modules/preact/compat/"],
16 | "react-dom": ["./node_modules/preact/compat/"]
17 | }
18 | },
19 | "include": ["node_modules/vite/client.d.ts", "**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/backend/ponk/migrations/0002_delete_bonkevent_remove_user_bonk_token.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.4 on 2024-06-02 15:50
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("ponk", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.DeleteModel(
14 | name="BonkEvent",
15 | ),
16 | migrations.RemoveField(
17 | model_name="user",
18 | name="bonk_token",
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/startup.d/14-tmux:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$prod" = false ]; then
4 | tmux set-option -ga ' CLIENT_ID CLIENT_SECRET VITE_STRIPE_API_KEY STRIPE_WEBHOOK_KEY DB_NAME DB_PASS DB_USER FT_DEBUG'
5 | tmux new-session -d 'trap : INT; make || $SHELL'
6 | tmux split-window -h 'trap : INT; make bonk-server || $SHELL'
7 | tmux split-window -vb 'trap : INT; $SHELL'
8 | tmux set -g mouse on # neat
9 | tmux split-window -h 'trap : INT; make fdev || $SHELL'
10 | tmux split-window -v 'trap : INT; make pong-server || $SHELL'
11 | tmux attach
12 | fi
13 |
--------------------------------------------------------------------------------
/backend/pong-server/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "pong-server"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | axum = { version = "0.7.4", features = ["ws"] }
8 | futures = "0.3.30"
9 | glam = { version = "0.27.0", features = ["serde"] }
10 | mint = { version = "0.5.9", features = ["serde"] }
11 | rand = "0.8.5"
12 | reqwest = { version = "0.12.4", features = ["json"] }
13 | serde = { version = "1.0.198", features = ["derive"] }
14 | serde_json = "1.0.116"
15 | tokio = { version = "1.36.0", features = ["full"] }
16 | uuid = { version = "1.8.0", features = ["fast-rng", "v4"] }
17 |
--------------------------------------------------------------------------------
/backend/ponk/utils.py:
--------------------------------------------------------------------------------
1 | from ponk.models import User
2 | from ponk.money import skins
3 | import random
4 |
5 | DEFAULT_SKINS = [
6 | "/assets/skins/red.png",
7 | "/assets/skins/green.png",
8 | "/assets/skins/blue.png",
9 | ]
10 |
11 |
12 | def get_selected_skin_url(username):
13 | user = User.objects.get(username=username)
14 | if user.selected_skin == "":
15 | return random.choice(DEFAULT_SKINS)
16 |
17 | skin = None
18 | for item in skins:
19 | if item["name"] == user.selected_skin:
20 | skin = item
21 |
22 | return random.choice(skin["images"])
23 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import preact from "@preact/preset-vite";
3 | import path from "path";
4 | import wasm from "vite-plugin-wasm-pack";
5 |
6 | const resolve = (f) => path.resolve(__dirname, f);
7 |
8 | export default defineConfig({
9 | plugins: [preact(), wasm("./pong-client")],
10 | base: "/static/",
11 | build: {
12 | manifest: true,
13 | outDir: resolve("./build"),
14 | rollupOptions: {
15 | input: {
16 | index: resolve("src/index.jsx"),
17 | },
18 | },
19 | assetsDir: ".", // instead of assets/...
20 | },
21 | envDir: "..",
22 | });
23 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/Level.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { language } from '../../scripts/languages';
3 | import { LangContext } from '../../Contexts';
4 |
5 |
6 | export function Level({level, levelPercentage}) {
7 | const lang = useContext(LangContext);
8 | return (
9 |
10 | {language.level[lang]} {level}
11 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/components/Shop/BallPresentation.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function BallPresentation(props) {
4 | return (
5 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/backend/bonk-server/settings.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | var server_ip: String
4 | var server_port: int
5 | const server_port_listen: int = 9999
6 |
7 | var server_protocol: String
8 | var api_ip: String
9 | const api_port: int = 8000
10 | var api_path: String
11 |
12 | const force_factor: int = 400 # move force
13 | const impulse_factor: int = 200 #jump force
14 | const bounce: float = 0.2 # bounce factor when colliding other players
15 | const max_grapple_radius: int = 300 # maximum grapple distance
16 | const power_time:int = 4 # time in seconds from full endurance to tired
17 | const tired_time:int = 5 # time in seconds of tired state
18 | const parallax_speed:int = 100 # scroll speed of paralax background
19 |
--------------------------------------------------------------------------------
/backend/ponk/migrations/0003_alter_image_image_alter_user_avatar.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.4 on 2024-06-05 23:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("ponk", "0002_delete_bonkevent_remove_user_bonk_token"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="image",
15 | name="image",
16 | field=models.ImageField(upload_to="assets/avatars/"),
17 | ),
18 | migrations.AlterField(
19 | model_name="user",
20 | name="avatar",
21 | field=models.URLField(default="/assets/sheldon.png"),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "scripts": {
5 | "wasm": "wasm-pack build ./pong-client --target web",
6 | "dev": "pnpm wasm && vite",
7 | "build": "vite build"
8 | },
9 | "dependencies": {
10 | "apexcharts": "^3.49.0",
11 | "preact": "^10.13.1",
12 | "preact-iso": "^2.6.2",
13 | "react-apexcharts": "^1.4.1"
14 | },
15 | "devDependencies": {
16 | "@preact/preset-vite": "^2.8.2",
17 | "autoprefixer": "^10.4.19",
18 | "eslint": "^8.57.0",
19 | "eslint-config-preact": "^1.3.0",
20 | "postcss": "^8.4.38",
21 | "tailwindcss": "^3.4.3",
22 | "vite": "^4.5.3",
23 | "vite-plugin-wasm-pack": "^0.1.12"
24 | },
25 | "eslintConfig": {
26 | "extends": "preact"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ponk.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/backend/ponk/templatetags/setting.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings as django_settings
3 | from django.utils.safestring import mark_safe
4 |
5 |
6 | class UnknownSetting(BaseException):
7 | pass
8 |
9 |
10 | register = template.Library()
11 |
12 |
13 | @register.simple_tag
14 | @mark_safe
15 | def settings():
16 | return f''
17 |
18 |
19 | @register.simple_tag
20 | @mark_safe
21 | def endsettings():
22 | return f" "
23 |
24 |
25 | @register.simple_tag
26 | @mark_safe
27 | def setting(name):
28 | if not hasattr(django_settings, name):
29 | raise UnknownSetting()
30 | value = getattr(django_settings, name)
31 | return f'{value} '
32 |
--------------------------------------------------------------------------------
/startup.d/04-init_db:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | if [ "$prod" = true ]; then
6 | mkdir -p backend/.pg
7 | chown 1000:1000 backend/.pg
8 | # i can't fucking sudo
9 | (
10 | echo '#include '
11 | echo '#include '
12 | echo '#define blblblbl return'
13 | echo 'uid_t geteuid(){blblblbl 1000;}'
14 | echo 'uid_t getuid(){blblblbl 1000;}'
15 | ) |\
16 | cc -shared -xc -
17 | export LD_PRELOAD=./a.out
18 | fi
19 |
20 | initdb #2>/dev/null
21 | pg_ctl -o "-k /tmp" -l backend/pg.log start
22 | createdb -h /tmp "$DB_NAME" 2>/dev/null
23 | psql -h /tmp -c "
24 | CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
25 | GRANT ALL ON DATABASE $DB_NAME TO $DB_USER;
26 | ALTER DATABASE $DB_NAME OWNER TO $DB_USER;" \
27 | "$DB_NAME"
28 |
29 | unset LD_PRELOAD
30 | rm a.out
31 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | transcendence:
3 | build:
4 | context: docker
5 | dockerfile: transcendence
6 | volumes:
7 | - .:/app
8 | - assets:/app/assets/avatar
9 | ports:
10 | - "8443:8443"
11 | - "8000:8000"
12 | depends_on:
13 | - pong-server
14 | environment:
15 | - HOST_IP_ADDRESS
16 | pong-server:
17 | volumes:
18 | - .:/app
19 | build:
20 | context: docker
21 | dockerfile: pong-server
22 | ports:
23 | - "4210:4210"
24 | bonk-server:
25 | volumes:
26 | - .:/app
27 | build:
28 | context: docker
29 | dockerfile: bonk-server
30 | ports:
31 | - "9999:9999" # do we really have to expose the ports?
32 | # they are proxied by caddy...
33 |
34 | volumes:
35 | assets:
36 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/Ubuntu-Regular.ttf.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="font_data_dynamic"
4 | type="FontFile"
5 | uid="uid://bwoq7xiu7yhey"
6 | path="res://.godot/imported/Ubuntu-Regular.ttf-ee796fcae923fc27d316973a15c8fd80.fontdata"
7 |
8 | [deps]
9 |
10 | source_file="res://asset/Ubuntu-Regular.ttf"
11 | dest_files=["res://.godot/imported/Ubuntu-Regular.ttf-ee796fcae923fc27d316973a15c8fd80.fontdata"]
12 |
13 | [params]
14 |
15 | Rendering=null
16 | antialiasing=1
17 | generate_mipmaps=false
18 | multichannel_signed_distance_field=false
19 | msdf_pixel_range=8
20 | msdf_size=48
21 | allow_system_fallback=true
22 | force_autohinter=false
23 | hinting=1
24 | subpixel_positioning=1
25 | oversampling=0.0
26 | Fallbacks=null
27 | fallbacks=[]
28 | Compress=null
29 | compress=true
30 | preload=[]
31 | language_support={}
32 | script_support={}
33 | opentype_features={}
34 |
--------------------------------------------------------------------------------
/backend/ponk/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for ponk project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 | from channels.auth import AuthMiddlewareStack
14 | from channels.routing import ProtocolTypeRouter, URLRouter
15 | from channels.security.websocket import AllowedHostsOriginValidator
16 |
17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ponk.settings")
18 |
19 | import ponk.chat
20 |
21 | application = ProtocolTypeRouter(
22 | {
23 | "http": get_asgi_application(),
24 | "websocket": AllowedHostsOriginValidator(
25 | AuthMiddlewareStack(URLRouter(ponk.chat.urls))
26 | ),
27 | }
28 | )
29 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/PopUp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Card } from "./Card";
3 |
4 | export function PopUp({
5 | active,
6 | setActive,
7 | children,
8 | className = "",
9 | clear = () => {},
10 | onKeyPress = () => {},
11 | }) {
12 | function handleClick() {
13 | setActive(false);
14 | clear();
15 | }
16 | return (
17 |
20 |
24 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/backend/ponk/api_decorators.py:
--------------------------------------------------------------------------------
1 | from django.http.response import JsonResponse
2 | import os
3 |
4 |
5 | def authenticated(endpoint):
6 | def wrapper(request, *args, **kwargs):
7 | if request.user.is_authenticated:
8 | return endpoint(request, args, kwargs)
9 | return JsonResponse(
10 | {
11 | "error": "You need to be logged in",
12 | },
13 | status=401,
14 | )
15 |
16 | return wrapper
17 |
18 |
19 | def private_api_auth(original):
20 | def wrapper(request, *args, **kwargs):
21 | auth = request.headers.get("Authorization")
22 | if auth != os.environ["PRIVATE_API_TOKEN"]:
23 | return JsonResponse(
24 | {
25 | "error": "no",
26 | },
27 | status=403,
28 | )
29 | return original(request, args, kwargs)
30 |
31 | return wrapper
32 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VIRTUALENV = virtualenv
2 | PYTHON = python3
3 | PNPM = pnpm
4 |
5 | run:
6 | $(PYTHON) backend/manage.py runserver 0.0.0.0:8001
7 |
8 | fdev:
9 | $(PNPM) -C frontend run dev
10 |
11 | pong-server:
12 | $(MAKE) -C backend/$@
13 |
14 | venv:
15 | $(RM) -r $@
16 | $(VIRTUALENV) $@
17 |
18 | bonk-rebuild:
19 | godot4 --display-driver headless --path backend/bonk-server --export-debug server $(PWD)/backend/bonk-server/pkg/bonk-server.x86-64
20 | godot4 --display-driver headless --path backend/bonk-server --export-debug Web $(PWD)/frontend/bonk-client/bonk-client.html
21 | gzip -f frontend/bonk-client/bonk-client.wasm
22 | gzip -f frontend/bonk-client/bonk-client.pck
23 | yes yes | python backend/manage.py collectstatic
24 | sh backend/bonk-server/pkg/bonk-server.sh
25 |
26 | bonk-server:
27 | chmod +x backend/bonk-server/pkg/bonk-server.x86-64
28 | ./backend/bonk-server/pkg/bonk-server.x86-64
29 |
30 | .PHONY: run venv pong-server
--------------------------------------------------------------------------------
/backend/bonk-server/asset/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/circle_full.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://cpf1tsp2wymug"
6 | path="res://.godot/imported/circle_full.png-8814fc0bea25d80539d2720367194fb2.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://asset/circle_full.png"
14 | dest_files=["res://.godot/imported/circle_full.png-8814fc0bea25d80539d2720367194fb2.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar/LogCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { language } from '../../scripts/languages';
3 | import { LangContext, ProfileContext } from '../../Contexts';
4 |
5 |
6 | export function LogCard(props) {
7 | const profile = useContext(ProfileContext);
8 | const lang = useContext(LangContext);
9 |
10 | return (
11 |
12 |
13 |
14 |
{profile ? profile.name : null}
15 | {profile ? `${language.level[lang]} ${profile.level}` : null}
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asgiref==3.8.1
2 | attrs==23.2.0
3 | autobahn==23.6.2
4 | Automat==22.10.0
5 | black==23.9.1
6 | certifi==2024.2.2
7 | cffi==1.16.0
8 | channels==4.1.0
9 | charset-normalizer==3.3.2
10 | click==8.1.7
11 | codespell==2.2.6
12 | constantly==23.10.4
13 | cryptography==42.0.7
14 | daphne==4.1.2
15 | distlib==0.3.7
16 | Django==5.0.4
17 | django-vite==3.0.4
18 | docopt==0.6.2
19 | filelock==3.12.4
20 | gdtoolkit==3.3.1
21 | hyperlink==21.0.0
22 | idna==3.6
23 | incremental==22.10.0
24 | lark-parser==0.8.0
25 | mypy-extensions==1.0.0
26 | packaging==23.1
27 | pathspec==0.11.2
28 | platformdirs==3.10.0
29 | psycopg2==2.9.9
30 | pyasn1==0.6.0
31 | pyasn1_modules==0.4.0
32 | pycparser==2.22
33 | pyOpenSSL==24.1.0
34 | PyYAML==6.0.1
35 | regex==2023.8.8
36 | requests==2.31.0
37 | service-identity==24.1.0
38 | six==1.16.0
39 | sqlparse==0.4.4
40 | stripe==9.7.0
41 | Twisted==24.3.0
42 | txaio==23.1.1
43 | typing_extensions==4.11.0
44 | urllib3==2.2.1
45 | virtualenv==20.24.5
46 | zope.interface==6.4.post0
47 | pillow==10.3.0
--------------------------------------------------------------------------------
/frontend/pong-client/src/game.rs:
--------------------------------------------------------------------------------
1 | use serde::{
2 | Serialize,
3 | Deserialize,
4 | };
5 |
6 | use web_sys::Document;
7 |
8 | #[derive(Serialize, Deserialize, Clone)]
9 | pub struct OnConnectClient {
10 | pub id: String,
11 | username: String,
12 | }
13 |
14 | impl OnConnectClient {
15 | pub fn new(document: &Document, id: String) -> OnConnectClient {
16 |
17 | //replace with cookie to get username
18 | let old_username_input = document
19 | .get_element_by_id("username");
20 |
21 | let mut username = String::from("bob");
22 | if let Some(old_username_input) = old_username_input {
23 | username = old_username_input
24 | .get_attribute("name").unwrap();
25 | }
26 |
27 | OnConnectClient {
28 | id,
29 | username,
30 | }
31 | }
32 | }
33 |
34 | #[derive(Serialize, Deserialize, Clone)]
35 | pub struct Move {
36 | pub id: String,
37 | pub game_id: String,
38 | pub movement: String,
39 | }
40 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/city.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://byglgq480xi0k"
6 | path="res://.godot/imported/city.svg-cfdee767357c03dfc9c8e2b37e8d71b1.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://asset/city.svg"
14 | dest_files=["res://.godot/imported/city.svg-cfdee767357c03dfc9c8e2b37e8d71b1.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://dwu3aamu0wc2p"
6 | path="res://.godot/imported/icon.svg-635c9cd9560944100972280930a50328.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://asset/icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-635c9cd9560944100972280930a50328.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/circle.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://ir5g2j7r5tkt"
6 | path="res://.godot/imported/circle.svg-2d5dd87f94e1eee750aead598c95d689.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://asset/circle.svg"
14 | dest_files=["res://.godot/imported/circle.svg-2d5dd87f94e1eee750aead598c95d689.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/backend/ponk/debug.py:
--------------------------------------------------------------------------------
1 | # https://stackoverflow.com/questions/16337511/log-all-requests-from-the-python-requests-module
2 |
3 | import logging
4 | import contextlib
5 | from http.client import HTTPConnection
6 |
7 |
8 | def debug_requests_on():
9 | """Switches on logging of the requests module."""
10 | HTTPConnection.debuglevel = 1
11 |
12 | logging.basicConfig()
13 | logging.getLogger().setLevel(logging.DEBUG)
14 | requests_log = logging.getLogger("requests.packages.urllib3")
15 | requests_log.setLevel(logging.DEBUG)
16 | requests_log.propagate = True
17 |
18 |
19 | def debug_requests_off():
20 | """Switches off logging of the requests module, might be some side-effects"""
21 | HTTPConnection.debuglevel = 0
22 |
23 | root_logger = logging.getLogger()
24 | root_logger.setLevel(logging.WARNING)
25 | root_logger.handlers = []
26 | requests_log = logging.getLogger("requests.packages.urllib3")
27 | requests_log.setLevel(logging.WARNING)
28 | requests_log.propagate = False
29 |
30 |
31 | @contextlib.contextmanager
32 | def debug_requests():
33 | """Use with 'with'!"""
34 | debug_requests_on()
35 | yield
36 | debug_requests_off()
37 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% load django_vite %}
4 | {% load setting %}
5 |
6 | {% vite_hmr_client %}
7 |
8 |
9 |
10 |
11 | ft_transcendence
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {% vite_asset "src/index.jsx" %}
22 |
23 | {% settings %}
24 | {% setting "CLIENT_ID" %}
25 | {% setting "HOST" %}
26 | {% endsettings %}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/frontend/src/components/Chat/Message.jsx:
--------------------------------------------------------------------------------
1 | export function Message({ where, children }) {
2 | return (
3 |
7 | {where == "distant" ? (
8 |
9 | {children.author}
10 |
11 | ) : null}
12 |
13 | {where == "distant" ? (
14 |
17 |
18 | {children.level}
19 |
20 |
21 | ) : null}
22 |
30 | {children.content}
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/GameCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "../utils/Card";
3 |
4 | export function GameCard({ game, display, setDisplay }) {
5 | return display == "all" || display == game.game ? (
6 |
7 |
8 |
9 |
{
12 | display == game.game ? setDisplay("all") : setDisplay(game.game);
13 | }}
14 | >
15 | {game.game}
16 |
17 |
20 | + {game.xp}
21 | xp
22 |
23 |
26 | {game.score[0]} | {game.score[1]}
27 |
28 |
29 |
30 |
31 | ) : null;
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/pong-client/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "pong-client"
3 | version = "0.1.0"
4 | authors = ["vcornill"]
5 | edition = "2018"
6 |
7 | [lib]
8 | crate-type = ["cdylib", "rlib"]
9 |
10 | [dependencies]
11 | console_error_panic_hook = "0.1.7"
12 | glam = { version = "0.27.0", features = ["serde"] }
13 | js-sys = "0.3.69"
14 | serde = { version = "1.0.198", features = ["derive"] }
15 | serde_json = "1.0.116"
16 | uuid = { version = "1.8.0", features = ["js", "v4"] }
17 | wasm-bindgen = "0.2.92"
18 | wasm-bindgen-futures = "*"
19 | serde-wasm-bindgen = "0.4"
20 |
21 | [dependencies.web-sys]
22 | version = "0.3.4"
23 | features = [
24 | 'WebGlTexture',
25 | 'ReadableStream',
26 | 'Response',
27 | 'HtmlImageElement',
28 | 'WebGlUniformLocation',
29 | 'Location',
30 | 'Crypto',
31 | 'CssStyleDeclaration',
32 | 'MouseEvent',
33 | 'HtmlButtonElement',
34 | 'HtmlElement',
35 | 'Document',
36 | 'Element',
37 | 'HtmlCanvasElement',
38 | 'WebGlBuffer',
39 | 'WebGlVertexArrayObject',
40 | 'WebGl2RenderingContext',
41 | 'WebGlProgram',
42 | 'WebGlShader',
43 | 'Window',
44 | "BinaryType",
45 | "Blob",
46 | "ErrorEvent",
47 | "FileReader",
48 | "MessageEvent",
49 | "ProgressEvent",
50 | "WebSocket",
51 | "EventListener",
52 | "EventTarget",
53 | "KeyboardEvent"
54 | ]
55 |
56 |
57 | [profile.release]
58 | opt-level = "s"
59 |
--------------------------------------------------------------------------------
/backend/ponk/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | URL configuration for ponk project.
3 |
4 | The `urlpatterns` list routes URLs to views. For more information please see:
5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/
6 | Examples:
7 | Function views
8 | 1. Add an import: from my_app import views
9 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
10 | Class-based views
11 | 1. Add an import: from other_app.views import Home
12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13 | Including another URLconf
14 | 1. Import the include() function: from django.urls import include, path
15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16 | """
17 |
18 | from django.contrib import admin
19 | from django.urls import path, re_path, include
20 | from django.conf import settings
21 | from django.conf.urls.static import static
22 | from django.views.generic import TemplateView
23 | import ponk.chat
24 | import ponk.auth
25 | import ponk.api
26 |
27 | default = lambda url: re_path(".*", url)
28 |
29 | urlpatterns = [
30 | path("admin/", admin.site.urls),
31 | path("auth/", include(ponk.auth.urls)),
32 | path("api/", include(ponk.api.urls)),
33 | *ponk.chat.urls,
34 | *static(settings.ASSETS_URL, document_root=settings.ASSETS_ROOT),
35 | default(TemplateView.as_view(template_name="index.html")),
36 | ]
37 |
--------------------------------------------------------------------------------
/startup.d/08-caddy:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | if [ "$prod" = true ]; then
6 | export "optional_https_url=,\ https://localhost:8443"
7 | fi
8 |
9 | export "srv_root=$PWD/.static"
10 | export "assets_root=$PWD/assets"
11 |
12 | export "pong_upstream=localhost"
13 | export "bonk_upstream=localhost"
14 | export "private_api_upstream=localhost"
15 | if [ -f /.dockerenv ]; then
16 | export "pong_upstream=pong-server"
17 | export "bonk_upstream=bonk-server"
18 | export "private_api_upstream=transcendence"
19 | fi
20 |
21 | caddy start --adapter caddyfile --config <( cat <<-EOF
22 | {
23 | auto_https disable_redirects
24 | }
25 |
26 | http://$HOST_IP_ADDRESS:8000$optional_https_url
27 |
28 | handle_path /static/* {
29 | root * $srv_root
30 | file_server
31 | header {
32 | Cross-Origin-Opener-Policy same-origin
33 | Cross-Origin-Embedder-Policy require-corp
34 | }
35 | }
36 |
37 | handle_path /assets/* {
38 | root * $assets_root
39 | file_server
40 | }
41 |
42 | handle {
43 | header /bonk {
44 | Cross-Origin-Opener-Policy same-origin
45 | Cross-Origin-Embedder-Policy require-corp
46 | }
47 | reverse_proxy localhost:8001 # django
48 | header {
49 | Server bob
50 | }
51 | }
52 |
53 | handle_path /pong-ws {
54 | reverse_proxy $pong_upstream:4210
55 | }
56 |
57 |
58 | handle_path /bonk-ws {
59 | reverse_proxy $bonk_upstream:9999
60 | }
61 |
62 | log default {
63 | level FATAL
64 | }
65 | EOF
66 | )
67 |
--------------------------------------------------------------------------------
/backend/ponk/bonk.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.http import JsonResponse
3 | from django.views.decorators.csrf import csrf_exempt
4 | from ponk.api_decorators import private_api_auth
5 | from ponk.api_decorators import authenticated
6 | from ponk.models import User
7 | from ponk.private import events
8 |
9 | global_user_list = []
10 | room_id = 1
11 |
12 |
13 | @authenticated
14 | def join_matchmaking_bonk(request, *args, **kwargs):
15 | global room_id
16 | for i in range(len(global_user_list)):
17 | if request.user.username == global_user_list[i]:
18 | return JsonResponse({"error": "You are already in this game"}, status=409)
19 |
20 | global_user_list.append(request.user.username)
21 |
22 | if len(global_user_list) == 2:
23 | events.append({"game_id": str(room_id), "users": global_user_list.copy()})
24 | room_id += 1
25 | global_user_list.clear()
26 |
27 | return JsonResponse({"success": True})
28 |
29 |
30 | @csrf_exempt
31 | @private_api_auth
32 | def leave_matchmaking_bonk(request, *args, **kwargs):
33 | username = args[1].get("username")
34 | try:
35 | global_user_list.remove(username)
36 | return JsonResponse({"success": True})
37 | except ValueError:
38 | return JsonResponse({"error": "You are not in this game"}, status=409)
39 |
40 |
41 | urls = [
42 | path("join", join_matchmaking_bonk),
43 | path("leave/", leave_matchmaking_bonk),
44 | ]
45 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/GameHistory.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react';
2 | import { GameCard } from './GameCard';
3 | import { Card } from '../utils/Card';
4 | import { language } from '../../scripts/languages';
5 | import { LangContext } from '../../Contexts';
6 |
7 | export function GameHistory({gameHistory}) {
8 | const lang = useContext(LangContext);
9 |
10 | const [displayGame, setDisplayGame] = useState('all');
11 | const [loaded, setLoaded] = useState(false);
12 |
13 | useEffect(() => {
14 | setLoaded(true);
15 | }, []);
16 | return (
17 |
18 |
19 | {language.game_history[lang]}
20 |
21 |
24 | {gameHistory.map((game) => (
25 |
30 | ))}
31 | {!gameHistory.length ? (
32 |
33 |
34 | {language.game_history_default[lang]}
35 |
36 |
37 | ) : null}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/src/pages/Tournament/Waiting.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Card } from "../../components/utils/Card";
3 |
4 | export function Waiting() {
5 | const [suspension, setSuspension] = useState(".");
6 |
7 | useEffect(() => {
8 | const intervalId = setInterval(() => {
9 | setSuspension((current) => {
10 | if (current != "...") return `${current}.`;
11 | else return ".";
12 | });
13 | }, 900);
14 | return () => clearInterval(intervalId);
15 | }, []);
16 |
17 | return (
18 |
19 | Waiting{suspension}
20 | All participants must complete their game !
21 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/components/Shop/ShopItem.jsx:
--------------------------------------------------------------------------------
1 | import React, {useContext, useState} from 'react';
2 | import { PopUp } from '../utils/PopUp';
3 | import { Card } from '../utils/Card';
4 | import { ItemSelection } from './ItemSelection';
5 | import { Price } from './Price';
6 | import { BallPresentation } from './BallPresentation';
7 | import { language } from '../../scripts/languages';
8 | import { LangContext } from '../../Contexts';
9 |
10 |
11 | export function ShopItem(props) {
12 | const [popUp, setPopUp] = useState(false);
13 |
14 | const lang = useContext(LangContext);
15 |
16 | return (
17 |
18 | {popUp ?
19 |
20 |
21 | : null
22 | }
23 |
{setPopUp(true)}}>
24 |
25 |
26 | {props.item.name}
27 | {props.possessed ?
28 | {language.owned[lang]}
29 | :
30 | }
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/backend/bonk-server/global.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | var users: Array = []
4 | var rooms: Array = []
5 |
6 | func add_user(user: User):
7 | users.append(user)
8 |
9 | func remove_user(user: User):
10 | for room in rooms:
11 | if room.users.has(user):
12 | get_node("../main/rooms/" + room.name + "/players/" + str(user.id)).call_deferred("queue_free")
13 | room.users.erase(user)
14 | if room.users.size() == 0:
15 | print("Room removed: ", room.name)
16 | get_node("../main/rooms/" + room.name).call_deferred("queue_free")
17 | rooms.erase(room)
18 | users.erase(user)
19 |
20 | func get_user(arg):
21 | if arg is int:
22 | for user in users:
23 | if user.id == arg:
24 | return user
25 | elif arg is String:
26 | for user in users:
27 | if user.username == arg:
28 | return user
29 | return null
30 |
31 | func add_room(room: Room):
32 | print("Room added: ", room.name)
33 | rooms.append(room)
34 |
35 | func remove_room(room: Room):
36 | print("Room removed: ", room.name)
37 | for user in users:
38 | get_node("../main/rooms/" + room.name + "/players/" + str(user.id)).call_deferred("queue_free")
39 | room.users.erase(user)
40 | get_node("../main/rooms/" + room.name).call_deferred("queue_free")
41 | rooms.erase(room)
42 |
43 | func get_room(name):
44 | for room in rooms:
45 | if room.name == name:
46 | return room
47 | return null
48 |
49 | func get_free_room_layer():
50 | var used_layers = []
51 | for room in rooms:
52 | used_layers.append(room.layer)
53 | for i in range(32):
54 | if i not in used_layers:
55 | return i
56 | return -1
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/frontend/src/components/Tournament/InvitationCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Card } from "../utils/Card";
3 | import { language } from "../../scripts/languages";
4 | import { LangContext } from "../../Contexts";
5 | import { useLocation } from "preact-iso";
6 |
7 | export function InvitationCard({ room, setError }) {
8 | const location = useLocation();
9 | const lang = useContext(LangContext);
10 |
11 | function joinTournament() {
12 | fetch(`/api/tournament/join_room/${room.host_name}`, {
13 | method: "POST",
14 | })
15 | .then((res) => {
16 | if (!res.ok) {
17 | return res.json().then((errData) => {
18 | throw new Error(errData.error);
19 | });
20 | }
21 | location.route("/tournament/room");
22 | })
23 | .catch((err) => {
24 | setError(err.message);
25 | });
26 | }
27 |
28 | return (
29 |
30 |
35 |
38 |
39 |
40 | {room.room_name}
41 |
42 |
43 | {room.players_number}/{room.size} {language.players[lang]}
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/backend/ponk/ftapi.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from dataclasses import dataclass
3 | import requests
4 |
5 |
6 | class APIException(BaseException):
7 | pass
8 |
9 |
10 | BadStatusCodeException = APIException("Non-OK status code in response")
11 |
12 |
13 | def oauth_token(code):
14 | req = requests.post(
15 | f"{settings.API_URL}/oauth/token",
16 | data={
17 | "grant_type": "authorization_code",
18 | "client_id": settings.CLIENT_ID,
19 | "client_secret": settings.CLIENT_SECRET,
20 | "redirect_uri": settings.REDIRECT_URI,
21 | "code": code,
22 | },
23 | )
24 |
25 | if req.status_code != 200:
26 | raise BadStatusCodeException
27 |
28 | res = req.json()
29 | if not "access_token" in res:
30 | return APIException("Invalid JSON response")
31 |
32 | return res["access_token"]
33 |
34 |
35 | @dataclass
36 | class FtUser:
37 | login: str
38 | display_name: str
39 | avatar: str
40 |
41 | @classmethod
42 | def from_json(klass, data):
43 | if not "login" in data or not "displayname" in data or not "image" in data:
44 | raise APIException("Invalid /v2/me response")
45 |
46 | return klass(
47 | login=data["login"],
48 | display_name=data["displayname"],
49 | avatar=data["image"]["versions"]["small"],
50 | )
51 |
52 |
53 | def user_info(token):
54 | req = requests.get(
55 | f"{settings.API_URL}/v2/me", headers={"Authorization": f"Bearer {token}"}
56 | )
57 |
58 | if req.status_code != 200:
59 | raise BadStatusCodeException
60 |
61 | return FtUser.from_json(req.json())
62 |
--------------------------------------------------------------------------------
/backend/bonk-server/main.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=5 format=3 uid="uid://bx0c3wgr40qrr"]
2 |
3 | [ext_resource type="Script" path="res://main.gd" id="1_cglf0"]
4 | [ext_resource type="PackedScene" uid="uid://3psjlgsy78cg" path="res://map/map.tscn" id="2_xxo4d"]
5 | [ext_resource type="PackedScene" uid="uid://ecq8tbk7vkc0" path="res://player/player.tscn" id="3_mx2bi"]
6 | [ext_resource type="FontFile" uid="uid://bwoq7xiu7yhey" path="res://asset/Ubuntu-Regular.ttf" id="4_bkqya"]
7 |
8 | [node name="main" type="Control"]
9 | layout_mode = 3
10 | anchors_preset = 15
11 | anchor_right = 1.0
12 | anchor_bottom = 1.0
13 | grow_horizontal = 2
14 | grow_vertical = 2
15 | script = ExtResource("1_cglf0")
16 | map_scene = ExtResource("2_xxo4d")
17 | player_scene = ExtResource("3_mx2bi")
18 |
19 | [node name="message" type="Label" parent="."]
20 | layout_mode = 0
21 | offset_top = 310.0
22 | offset_right = 1280.0
23 | offset_bottom = 410.0
24 | theme_override_fonts/font = ExtResource("4_bkqya")
25 | theme_override_font_sizes/font_size = 24
26 | text = "Connecting to server..."
27 | horizontal_alignment = 1
28 | vertical_alignment = 1
29 |
30 | [node name="token" type="HTTPRequest" parent="."]
31 |
32 | [node name="authenticate" type="HTTPRequest" parent="."]
33 |
34 | [node name="event" type="HTTPRequest" parent="."]
35 |
36 | [node name="join" type="HTTPRequest" parent="."]
37 |
38 | [node name="leave" type="HTTPRequest" parent="."]
39 |
40 | [node name="game_stats" type="HTTPRequest" parent="."]
41 |
42 | [node name="rooms" type="Node" parent="."]
43 |
44 | [connection signal="request_completed" from="token" to="." method="token_request_completed"]
45 | [connection signal="request_completed" from="event" to="." method="event_request_completed"]
46 |
--------------------------------------------------------------------------------
/backend/ponk/models.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from django.db import models
3 | from django.contrib.auth.models import AbstractUser
4 | from django.contrib.postgres.fields import ArrayField
5 | from django.utils import timezone
6 |
7 | DEFAULT_AVATAR = "/assets/sheldon.png"
8 |
9 |
10 | class AuthMethod(models.IntegerChoices):
11 | BASIC = 1
12 | FT = 2
13 |
14 |
15 | class User(AbstractUser):
16 | friends = models.ManyToManyField("self", symmetrical=True, blank=True)
17 | friend_requests = models.ManyToManyField("self", symmetrical=False, blank=True)
18 | level = models.PositiveIntegerField(default=0)
19 | level_percentage = models.PositiveIntegerField(default=0)
20 | avatar = models.URLField(default=DEFAULT_AVATAR)
21 | skins = ArrayField(models.CharField(), default=list)
22 | selected_skin = models.CharField(default="")
23 | auth_method = models.IntegerField(choices=AuthMethod.choices)
24 | last_online = models.DateTimeField(default=timezone.now)
25 | citation = models.CharField(default="", max_length=256)
26 |
27 | def __str__(self):
28 | return f"User(username={self.username}"
29 |
30 |
31 | class GameHistory(models.Model):
32 | user = models.ForeignKey(
33 | User, on_delete=models.CASCADE, related_name="game_history"
34 | )
35 | game = models.CharField(max_length=32)
36 | score = ArrayField(models.PositiveIntegerField(), size=2)
37 | win = models.BooleanField()
38 | xp = models.PositiveIntegerField(default=0)
39 |
40 | def __str__(self):
41 | return f"{self.game} - {'Win' if self.win else 'Loss'}"
42 |
43 |
44 | class Image(models.Model):
45 | image = models.ImageField(upload_to="assets/avatars/")
46 |
47 | def __str__(self):
48 | return self.name
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ft_transcendence
2 |
3 | ## Introduction
4 |
5 | Best transcendence ever, made by students from 42LeHavre.
6 | The goal of this project is to make a single page application displaying at least a Pong game.
7 | But we decided to get more ambitious...
8 | 
9 |
10 | ## General Requirements
11 |
12 | - Create an API app on 42 intra
13 | - Edit your redirect URI to `https://localhost:8443/auth/42` in production mode or to `http://localhost:8000/auth/42` in dev mode
14 | - Copy .env.example into a .env file
15 |
16 | ## Development Usage
17 |
18 | - `nix-shell`
19 | - `python backend/manage.py migrate`
20 |
21 | ## Production Usage
22 |
23 | - `docker-compose up`
24 |
25 | ## Technologies
26 |
27 | - React
28 | - Tailwind
29 | - Django
30 | - Docker
31 |
32 | ## Modules
33 |
34 | - Framework Backend **M**
35 | - Framework Front-end **m**
36 | - Database **m**
37 | - User management **M**
38 | - OAuth 2.0 - API 42 **M**
39 | - Networked multiplayer **M**
40 | - More than 2 players **M**
41 | - Another Game : Bonk **M**
42 | - Customization **m**
43 | - Chat **M**
44 | - Bot Pong **M**
45 | - Stats Dashboard **m**
46 | - GDPR **m**
47 | - Microservices **M**
48 | - Responsive **m**
49 | - Interbrowser **m**
50 | - Multiple language support **m**
51 | - Server side pong **M**
52 |
53 | **M**: major module - **m**: minor module
54 |
55 | ## Norm
56 |
57 | ### Commit messages
58 |
59 | Commit messages should apply to the following norm:
60 |
61 | - `- | ` : Deleted something
62 | - `~ | ` : Modified something
63 | - `+ | ` : Added something
64 |
65 | This commit format follow the [DinoFormat](https://github.com/DinoMalin/DinoFormat) rules
66 |
67 | ### Code Formatter
68 |
69 | - **Javascript**: Prettier
70 | - **Python**: Black
71 |
--------------------------------------------------------------------------------
/backend/ponk/end.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from ponk.models import GameHistory, User
3 | from django.http.response import JsonResponse
4 | from django.views.decorators.csrf import csrf_exempt
5 | from ponk.api_decorators import private_api_auth
6 | from ponk.tournament import game_ended
7 | from ponk.tournament import rooms
8 |
9 |
10 | @csrf_exempt
11 | @private_api_auth
12 | def game_stats(request, *args, **kwargs):
13 | try:
14 | data = json.loads(request.body)
15 | score_user = data["score"][0]
16 | score_opponent = data["score"][1]
17 |
18 | win = score_user > score_opponent
19 | multiplier = 10 if win else 1
20 |
21 | if score_user == 4 and score_opponent == 1:
22 | multiplier = 17.25
23 |
24 | try:
25 | xp = (
26 | max(score_user, score_opponent) / min(score_user, score_opponent)
27 | ) * multiplier
28 | except ZeroDivisionError:
29 | xp = (max(score_user, score_opponent) / 1) * multiplier + 5
30 |
31 | user = User.objects.get(username=data["player"])
32 | user.level_percentage += xp
33 | if user.level_percentage >= 100:
34 | user.level += user.level_percentage // 100
35 | user.level_percentage = user.level_percentage % 100
36 | user.save()
37 |
38 | if win and user in rooms:
39 | game_ended(user)
40 |
41 | GameHistory(
42 | user=user, game=data["game"], score=data["score"], win=win, xp=xp
43 | ).save()
44 |
45 | return JsonResponse(
46 | {
47 | "success": True,
48 | }
49 | )
50 | except BaseException as e:
51 | print(e, file=sys.stderr)
52 | return JsonResponse(
53 | {
54 | "error": "missing fields!!!!",
55 | },
56 | status=400,
57 | )
58 |
59 |
60 | urls = [
61 | path("game_stats", game_stats),
62 | ]
63 |
--------------------------------------------------------------------------------
/startup.d/12-bonk:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prod=$1
4 |
5 | set -x
6 |
7 | if [ "$prod" = false ]; then
8 | VERSION=$(godot4 --version | sed 's/stable.*/stable/')
9 |
10 | mkdir -p "$HOME/.local/share/godot/export_templates/$VERSION"
11 | if [ ! -f "$HOME/.local/share/godot/export_templates/$VERSION/web_debug.zip" ]; then
12 | wget --no-check-certificate "https://docs.google.com/uc?export=download&id=1TSWOhwXIGD6rxX-5V1ynXsjXUrWQy6xj" -O "$HOME/.local/share/godot/export_templates/$VERSION/web_debug.zip"
13 | fi
14 |
15 | if [ "$FT_DEBUG" = "y" ]; then
16 | if [ ! -f "$HOME/.local/share/godot/export_templates/$VERSION/linux_debug.x86_64" ]; then
17 | wget --no-check-certificate "https://docs.google.com/uc?export=download&id=1yIHSqEMwfKHfkKACKWsc3bGQhbG_Idm9" -O "$HOME/.local/share/godot/export_templates/$VERSION/linux_debug.x86_64"
18 | fi
19 | else
20 | if [ ! -f "$HOME/.local/share/godot/export_templates/$VERSION/linux_release.x86_64" ]; then
21 | wget --no-check-certificate "https://docs.google.com/uc?export=download&id=13bQKnJhl3wpOE_aloVCpZiWCUTzxAQbL" -O "$HOME/.local/share/godot/export_templates/$VERSION/linux_release.x86_64"
22 | fi
23 | fi
24 |
25 | mkdir -p backend/bonk-server/pkg
26 | mkdir -p frontend/bonk-client
27 |
28 | if [ "$FT_DEBUG" = "y" ]; then
29 | godot4 --display-driver headless --path backend/bonk-server --export-debug server "$PWD/backend/bonk-server/pkg/bonk-server.x86-64"
30 | else
31 | godot4 --display-driver headless --path backend/bonk-server --export-release server "$PWD/backend/bonk-server/pkg/bonk-server.x86-64"
32 | fi
33 | godot4 --display-driver headless --path backend/bonk-server --export-debug Web "$PWD/frontend/bonk-client/bonk-client.html"
34 |
35 | gzip -f "$PWD/frontend/bonk-client/bonk-client.wasm"
36 | gzip -f "$PWD/frontend/bonk-client/bonk-client.pck"
37 | fi
38 |
39 | set +x
40 |
41 | rm -rf .static
42 | # copy all static files to .static, to be served by caddy
43 | # since serving them by django is strongly discouraged
44 | yes yes | python backend/manage.py collectstatic
45 |
--------------------------------------------------------------------------------
/backend/bonk-server/asset/city.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Created by potrace 1.15, written by Peter Selinger 2001-2017
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/src/components/Tournament/PlayerCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Card } from "../utils/Card";
3 | import { language } from "../../scripts/languages";
4 | import { useState } from "preact/hooks";
5 | import { LangContext } from "../../Contexts";
6 |
7 | export function PlayerCard({ player, host, iAmAdmin }) {
8 | const lang = useContext(LangContext);
9 | const userIsAdmin = player.username === host;
10 | const [kicked, setKicked] = useState(false);
11 |
12 | const kickPlayer = (player) => {
13 | if (kicked) return;
14 |
15 | fetch(`/api/tournament/kick_user/${player.username}`).then((res) =>
16 | res.json().then(({ success }) => {
17 | if (success) setKicked(true);
18 | }),
19 | );
20 | };
21 |
22 | return (
23 | !kicked && (
24 |
25 |
26 |
29 |
30 |
31 | {player.username}
32 |
33 |
34 | {language.level[lang]} {player.level}
35 |
36 |
37 |
38 | {userIsAdmin ? (
39 |
40 |
41 |
42 | ) : (
43 | iAmAdmin && (
44 | kickPlayer(player)}
47 | >
48 |
49 |
50 | )
51 | )}
52 |
53 | )
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/src/components/Shop/ItemSelection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { BallPresentation } from "./BallPresentation";
3 | import { Price } from "./Price";
4 | import { Input } from "../utils/Input";
5 | import { CTA } from "../utils/CTA";
6 | import { language } from "../../scripts/languages";
7 | import { LangContext, ProfileContext } from "../../Contexts";
8 |
9 | export function ItemSelection(props) {
10 | const profile = useContext(ProfileContext);
11 | const lang = useContext(LangContext);
12 |
13 | function handleClick() {
14 | if (!props.selected)
15 | fetch(`/api/skin/${props.item.name}`).then(props.fetchProfile);
16 | else fetch(`/api/skin/default`).then(props.fetchProfile);
17 | }
18 |
19 | return (
20 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/Stat.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { Card } from "../utils/Card";
3 | // import Chart from 'chart.js/auto';
4 | import Chart from "react-apexcharts";
5 |
6 | export function Stat({ labels, data, colors, duration }) {
7 | const [size, setSize] = useState(window.screen.width < 768 ? 150 : 200);
8 |
9 | const percentage = Math.floor((data[0] * 100) / (data[0] + data[1]));
10 | const opt = {
11 | series: [isNaN(percentage) ? 50 : percentage],
12 | options: {
13 | plotOptions: {
14 | radialBar: {
15 | hollow: {
16 | size: "70%",
17 | background: "#2e2e2e",
18 | },
19 | track: {
20 | background: colors[1],
21 | strokeWidth: "100%",
22 | dropShadow: {
23 | enabled: true,
24 | top: 4,
25 | left: 4,
26 | blur: 8,
27 | opacity: 0.5,
28 | },
29 | },
30 | dataLabels: {
31 | name: {
32 | show: true,
33 | },
34 | value: {
35 | offsetY: 6,
36 | fontSize: "16px",
37 | color: "white",
38 | },
39 | },
40 | },
41 | },
42 | colors: [colors[0]],
43 | labels: [labels[0]],
44 | stroke: {
45 | lineCap: "round",
46 | },
47 | },
48 | };
49 |
50 | const [loaded, setLoaded] = useState(false);
51 |
52 | useEffect(() => {
53 | setLoaded(true);
54 | window.addEventListener("resize", () => {
55 | setSize(window.screen.width < 768 ? 150 : 200);
56 | });
57 | return () => {
58 | window.removeEventListener("resize", () => {
59 | setSize(window.screen.width < 768 ? 150 : 200);
60 | });
61 | };
62 | }, []);
63 |
64 | return (
65 |
68 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | with import {};
2 |
3 | {
4 | prod ? false,
5 | should-start ? "transcendence"
6 | }:
7 |
8 | let
9 | fenix = import (fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz") { };
10 | commonDeps = [
11 | # rust
12 | (with fenix; with latest; combine [
13 | minimal.toolchain
14 | targets.wasm32-unknown-unknown.latest.rust-std
15 | ])
16 | wasm-pack
17 |
18 | # kil me pls
19 | postgresql
20 | pkgs.glibcLocales
21 |
22 | python3
23 | python3Packages.pip
24 | virtualenv
25 | gnused
26 |
27 | glibc
28 | ];
29 | transcendenceDeps = lib.optional (should-start == "transcendence") [
30 | caddy
31 |
32 | nodejs
33 | nodePackages.pnpm
34 |
35 | openssh
36 | ];
37 | pongDeps = lib.optional (should-start == "pong-server") [
38 | # hmmm
39 | ];
40 | bonkDeps = lib.optional (should-start == "bonk-server") [
41 | wget
42 | godot_4
43 | ];
44 | nonProdDeps = lib.optional (!prod) [
45 | codespell
46 | nodePackages.prettier
47 | gdtoolkit
48 | black
49 | tmux
50 | wget
51 | godot_4
52 | ];
53 | in
54 |
55 | mkShell {
56 | packages = commonDeps ++ transcendenceDeps ++ pongDeps ++ bonkDeps ++ nonProdDeps;
57 |
58 | nativeBuildInputs = [ pkgs.pkg-config ];
59 | LD_LIBRARY_PATH = lib.makeLibraryPath [ openssl ];
60 | buildInputs = [ pkgs.openssl ];
61 |
62 | shellHook = ''
63 | should_start="${should-start}"
64 |
65 | case "$should_start" in
66 | pong-server|bonk-server)
67 | for script in startup.d/02-source_env startup.d/03-env startup.d/09-debug; do
68 | echo sourcing $script...
69 | ${if prod then
70 | ''
71 | source "$script" true
72 | ''
73 | else
74 | ''
75 | source "$script" false
76 | ''
77 | }
78 | done
79 |
80 | if [ "$should_start" = bonk-server ]; then
81 | source startup.d/06-venv true
82 | source startup.d/12-bonk false
83 | fi
84 |
85 | make "$should_start"
86 | ;;
87 | transcendence)
88 | for script in startup.d/*; do
89 | echo sourcing $script...
90 | ${if prod then
91 | ''
92 | source "$script" true
93 | ''
94 | else
95 | ''
96 | source "$script" false
97 | ''
98 | }
99 | done
100 | ;;
101 | *)
102 | echo invalid --arg should-start value >&2
103 | exit 1
104 | ;;
105 | esac
106 | '';
107 | }
108 |
--------------------------------------------------------------------------------
/backend/ponk/money.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.http.response import JsonResponse
3 | from django.views.decorators.csrf import csrf_exempt
4 | from ponk.models import User
5 | import stripe
6 | import json
7 | import sys
8 | import os
9 |
10 |
11 | stripe.api_key = os.environ["VITE_STRIPE_API_KEY"]
12 | webhook_secret = os.environ["STRIPE_WEBHOOK_KEY"]
13 | skins = json.load(open(os.path.dirname(os.path.realpath(__file__)) + "/skins.json"))
14 |
15 |
16 | @csrf_exempt
17 | def money_webhook(request, *args, **kwargs):
18 | sig = request.headers.get("Stripe-Signature", None)
19 |
20 | try:
21 | event = stripe.Webhook.construct_event(request.body, sig, webhook_secret)
22 | except BaseException:
23 | return JsonResponse(
24 | {
25 | "error": "rip",
26 | },
27 | status=400,
28 | )
29 |
30 | if event.type == "checkout.session.completed":
31 | data = event.data.object
32 | username_and_skin = data.client_reference_id
33 |
34 | if not username_and_skin or not "_" in username_and_skin:
35 | print(
36 | "client_reference_id is empty!! (I don't know who bought the skin)",
37 | file=sys.stderr,
38 | )
39 | return JsonResponse(
40 | {
41 | "error": "empty client_reference_id",
42 | },
43 | status=400,
44 | )
45 |
46 | username_and_skin_raw = username_and_skin.split("_")
47 | username = username_and_skin_raw[0]
48 | skin = username_and_skin_raw[1]
49 |
50 | try:
51 | user = User.objects.get(username=username)
52 | for available_skin in skins:
53 | if available_skin["name"] == skin:
54 | user.skins.append(skin)
55 | user.save()
56 | return JsonResponse({"success": True})
57 |
58 | return JsonResponse(
59 | {
60 | "error": "unknown skin",
61 | },
62 | status=400,
63 | )
64 |
65 | except User.DoesNotExist:
66 | return JsonResponse(
67 | {
68 | "error": "user does not exist",
69 | },
70 | status=400,
71 | )
72 |
73 | return JsonResponse(
74 | {
75 | "yeah yeah send me other events": True,
76 | }
77 | )
78 |
79 |
80 | urls = [
81 | path("payments", money_webhook),
82 | ]
83 |
--------------------------------------------------------------------------------
/frontend/src/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | body {
7 | font-family: "Ubuntu", sans-serif;
8 | }
9 |
10 | .background-animate {
11 | background-size: 200%;
12 |
13 | -webkit-animation: AnimationName 7s linear infinite;
14 | -moz-animation: AnimationName 7s linear infinite;
15 | animation: AnimationName 7s linear infinite;
16 | }
17 |
18 | @keyframes AnimationName {
19 | 0%,
20 | 100% {
21 | background-position: 0% 0%;
22 | }
23 | 50% {
24 | background-position: 100% 100%;
25 | }
26 | }
27 |
28 | .angle-gradient {
29 | background: linear-gradient(
30 | 120deg,
31 | rgba(255, 255, 255, 0.2),
32 | rgba(255, 255, 255, 0.05)
33 | );
34 | }
35 |
36 | .border-gradient-hover:hover:before {
37 | content: "";
38 | position: absolute;
39 | inset: 0;
40 | padding: 2px;
41 | background: linear-gradient(
42 | to bottom right,
43 | #f6eaff,
44 | #dfd2f0,
45 | #cebde3,
46 | #d3b4fb
47 | );
48 | -webkit-mask:
49 | linear-gradient(#fff 0 0) content-box,
50 | linear-gradient(#fff 0 0);
51 | mask:
52 | linear-gradient(#fff 0 0) content-box,
53 | linear-gradient(#fff 0 0);
54 | -webkit-mask-composite: xor;
55 | mask-composite: exclude;
56 | pointer-events: none;
57 | }
58 | .border-gradient:before {
59 | content: "";
60 | position: absolute;
61 | inset: 0;
62 | padding: 2px;
63 | background: linear-gradient(
64 | to bottom right,
65 | #f6eaff,
66 | #dfd2f0,
67 | #cebde3,
68 | #d3b4fb
69 | );
70 | -webkit-mask:
71 | linear-gradient(#fff 0 0) content-box,
72 | linear-gradient(#fff 0 0);
73 | mask:
74 | linear-gradient(#fff 0 0) content-box,
75 | linear-gradient(#fff 0 0);
76 | -webkit-mask-composite: xor;
77 | mask-composite: exclude;
78 | pointer-events: none;
79 | }
80 | .nav:before {
81 | border-radius: 4242px;
82 | }
83 |
84 | .text-shadow {
85 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
86 | }
87 |
88 | .bvambient_particle {
89 | position: absolute;
90 | pointer-events: none;
91 | transition:
92 | top linear,
93 | left linear;
94 | }
95 |
96 | .down-gradient {
97 | -webkit-mask-image: -webkit-gradient(
98 | linear,
99 | left top,
100 | left bottom,
101 | color-stop(80%, rgba(0, 0, 0, 1)),
102 | to(rgba(0, 0, 0, 0))
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/backend/bonk-server/map/map.gd:
--------------------------------------------------------------------------------
1 | extends Node2D
2 |
3 | @export var layer : int = 0
4 | signal game_finished(room_name, winner, scores)
5 | var started = false
6 | var finished = false
7 |
8 | func start_round(message, time):
9 | for player in get_node("players").get_children():
10 | player.get_node("body").set_position(Vector2(640, 235))
11 | player.get_node("body").set_freeze_enabled(true)
12 | player.get_node("body").death_state = false
13 | player.get_node("body").tired_state = false
14 | player.get_node("body").grapple_state = false
15 | player.get_node("body").power = 100
16 | get_node("message").set_text(message)
17 | await get_tree().create_timer(3).timeout
18 | while time >= 0:
19 | get_node("message").set_text(str(time))
20 | time -= 1
21 | await get_tree().create_timer(1).timeout
22 | get_node("message").set_text("")
23 | for player in get_node("players").get_children():
24 | player.get_node("body").set_freeze_enabled(false)
25 | started = true
26 |
27 |
28 | func _ready():
29 | if multiplayer.is_server():
30 | get_node("body").set_collision_layer(1 << layer)
31 | get_node("body").set_collision_mask(1 << layer)
32 | var physic = PhysicsMaterial.new()
33 | physic.set_absorbent(true)
34 | physic.set_bounce(Settings.bounce)
35 | get_node("body").set_physics_material_override(physic)
36 | start_round("Ready ?", 3)
37 |
38 | func _process(delta):
39 | if !multiplayer.is_server():
40 | get_node("background").scroll_offset += Vector2(1, 0) * Settings.parallax_speed * delta
41 | elif started:
42 | if finished:
43 | return
44 | var death_count = 0
45 | var winner = null
46 | var scores = ""
47 | var scores_dict = {}
48 | for player in get_node("players").get_children():
49 | scores_dict[player.get_node("body/username").get_text()] = player.get_node("body").score
50 | scores += player.get_node("body/username").get_text() + ": " + str(player.get_node("body").score) + "\n"
51 | if player.get_node("body").death_state:
52 | death_count += 1
53 | else:
54 | winner = player
55 | get_node("scores").set_text(scores)
56 | if !winner:
57 | start_round("Draw !", 3)
58 | elif death_count >= get_node("players").get_child_count() - 1:
59 | winner.get_node("body").score += 1
60 | if winner.get_node("body").score >= 5 || get_node("players").get_child_count() == 1:
61 | finished = true
62 | scores_dict[winner.get_node("body/username").get_text()] = winner.get_node("body").score
63 | game_finished.emit(get_name(), Global.get_user(winner.get_node("body/username").get_text()), scores_dict)
64 | else:
65 | start_round(winner.get_node("body/username").get_text() + " scored !", 3)
66 |
--------------------------------------------------------------------------------
/frontend/src/pages/LegalNotice/LegalNotice.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "../../components/utils/Card";
3 |
4 | export function LegalNotice() {
5 | return (
6 |
7 | MENTIONS LÉGALES
8 |
9 | Conformément aux dispositions de la loi n° 2004-575 du 21 juin 2004 pour
10 | la confiance en l'économie numérique, il est précisé aux utilisateurs du
11 | site Ponk l'identité des différents intervenants dans le cadre de sa
12 | réalisation et de son suivi.
13 |
14 | Edition du site
15 |
16 | Le présent site, accessible à l’URL www.ft-transcendence.fr (le « Site
17 | »), est édité par :
18 |
19 | - Julian Cario, résidant 3 rue des Stegosaures, 76600 Le Havre, de
20 | nationalité Française (France), né(e) le 31/12/2005,
21 |
22 |
23 | - Chay Ane Godard, résidant 2 rue des Abricots, 76600 Le Havre, de
24 | nationalité Française (France), né(e) le 20/01/2004,
25 |
26 |
27 | - Nils Laeremans, résidant 7 quai Arborescent, 76600 Le Havre, de
28 | nationalité Française (France), né(e) le 15/07/2004,
29 |
30 |
31 | - Gwechen Prigent, résidant 77 boulevard ChatGPT, 76600 Le Havre, de
32 | nationalité Française (France), né(e) le 01/02/2003,
33 |
34 |
35 | - Antoine Legay, résidant 14 rue Feur, 76600 Le Havre, de nationalité
36 | Française (France), né(e) le 11/09/2005,
37 |
38 |
39 | - Victor Cornille, résidant 302 bis rue Sadoïava, 76600 Le Havre, de
40 | nationalité Française (France), né(e) le 21/05/2001
41 |
42 |
43 | Hébergement
44 |
45 | Le Site est hébergé par la société Hostinger International LTD, situé ,
46 | (contact téléphonique ou email : https://www.hostinger.fr/contact).
47 |
48 | Directeur de publication
49 |
50 | Le Directeur de la publication du Site est DinoMalin.
51 |
52 | Nous contacter
53 |
54 |
jcario@student.42lehavre.fr
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/pages/Games/bonk.css:
--------------------------------------------------------------------------------
1 | body {
2 | touch-action: none;
3 | margin: 0;
4 | border: 0 none;
5 | padding: 0;
6 | text-align: center;
7 | background-color: black;
8 | }
9 |
10 | #canvas {
11 | display: block;
12 | margin: 0;
13 | color: white;
14 | }
15 |
16 | #canvas:focus {
17 | outline: none;
18 | }
19 |
20 | .godot {
21 | font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
22 | color: #e0e0e0;
23 | background-color: #3b3943;
24 | background-image: linear-gradient(to bottom, #403e48, #35333c);
25 | border: 1px solid #45434e;
26 | box-shadow: 0 0 1px 1px #2f2d35;
27 | }
28 |
29 | /* Status display */
30 |
31 | #status {
32 | position: absolute;
33 | left: 0;
34 | top: 0;
35 | right: 0;
36 | bottom: 0;
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | /* don't consume click events - make children visible explicitly */
41 | visibility: hidden;
42 | }
43 |
44 | #status-progress {
45 | width: 366px;
46 | height: 7px;
47 | background-color: #38363A;
48 | border: 1px solid #444246;
49 | padding: 1px;
50 | box-shadow: 0 0 2px 1px #1B1C22;
51 | border-radius: 2px;
52 | visibility: visible;
53 | }
54 |
55 | @media only screen and (orientation:portrait) {
56 | #status-progress {
57 | width: 61.8%;
58 | }
59 | }
60 |
61 | #status-progress-inner {
62 | height: 100%;
63 | width: 0;
64 | box-sizing: border-box;
65 | transition: width 0.5s linear;
66 | background-color: #202020;
67 | border: 1px solid #222223;
68 | box-shadow: 0 0 1px 1px #27282E;
69 | border-radius: 3px;
70 | }
71 |
72 | #status-indeterminate {
73 | height: 42px;
74 | visibility: visible;
75 | position: relative;
76 | }
77 |
78 | #status-indeterminate > div {
79 | width: 4.5px;
80 | height: 0;
81 | border-style: solid;
82 | border-width: 9px 3px 0 3px;
83 | border-color: #2b2b2b transparent transparent transparent;
84 | transform-origin: center 21px;
85 | position: absolute;
86 | }
87 |
88 | #status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
89 | #status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
90 | #status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
91 | #status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
92 | #status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
93 | #status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
94 | #status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
95 | #status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
96 |
97 | #status-notice {
98 | margin: 0 100px;
99 | line-height: 1.3;
100 | visibility: visible;
101 | padding: 4px 6px;
102 | visibility: visible;
103 | }
--------------------------------------------------------------------------------
/frontend/src/pages/Play/index.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from "preact-iso";
2 | import { useEffect, useState } from "preact/hooks";
3 |
4 | export function Play() {
5 | const [citation, setCitation] = useState(null);
6 | const location = useLocation();
7 |
8 | useEffect(() => {
9 | fetch("/api/citation").then((res) =>
10 | res.json().then((data) => {
11 | if (!data.error) setCitation(data);
12 | }),
13 | );
14 |
15 | if (!document.getElementById("lottie")) {
16 | const scriptEl = document.createElement("script");
17 |
18 | scriptEl.id = "lottie";
19 | scriptEl.src =
20 | "https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs";
21 | scriptEl.type = "module";
22 |
23 | document.body.appendChild(scriptEl);
24 | }
25 |
26 | const anchorPong = document.getElementById("anchor-pong");
27 | const anchorBonk = document.getElementById("anchor-bonk");
28 |
29 | const dotlottie = ` `;
35 |
36 | const dotlottieBlue = ` `;
42 |
43 | anchorPong.innerHTML += dotlottie;
44 | anchorBonk.innerHTML += dotlottieBlue;
45 | }, []);
46 |
47 | return (
48 |
49 | {citation ? (
50 |
51 | ❝{citation.citation}❞ - {citation.username}
52 |
53 | ) : null}
54 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/src/pages/Auth/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import { Card } from '../../components/utils/Card';
3 | import { Input } from '../../components/utils/Input';
4 | import { CTA } from '../../components/utils/CTA';
5 | import { language } from '../../scripts/languages';
6 | import { LangContext } from '../../Contexts';
7 |
8 | export function Signup(props) {
9 | const lang = useContext(LangContext);
10 |
11 | const username = React.useRef(null);
12 | const password = React.useRef(null);
13 | const confirmPassword = React.useRef(null);
14 |
15 | const [error, setError] = useState("");
16 |
17 | function handleSignup() {
18 | if (password.current.value != confirmPassword.current.value) {
19 | setError("Password mismatch !");
20 | password.current.value = "";
21 | confirmPassword.current.value = "";
22 | return;
23 | }
24 |
25 | fetch("/auth/register", {
26 | method: "POST",
27 | headers: {
28 | "Content-Type": "application/json",
29 | },
30 | body: JSON.stringify({
31 | username: username.current.value,
32 | password: password.current.value,
33 | }),
34 | })
35 | .then((res) => {
36 | if (!res.ok) {
37 | return res.json().then((errData) => {
38 | throw new Error(errData.error);
39 | });
40 | }
41 | })
42 | .then((data) => {
43 | window.location.pathname = "/play";
44 | })
45 | .catch((err) => {
46 | setError(err.message);
47 | username.current.value = "";
48 | password.current.value = "";
49 | confirmPassword.current.value = "";
50 | });
51 | }
52 |
53 | function enter(e) {
54 | if (e.key == "Enter") handleSignup();
55 | }
56 |
57 | return (
58 |
59 |
60 |
{language.sign_up[lang]}
61 |
67 |
{props.setTriedLog(true); handleSignup()}} className="my-2">{language.sign_up[lang]}
68 |
69 |
70 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/FriendCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import { Card } from '../utils/Card';
3 | import { language } from '../../scripts/languages';
4 | import { LangContext } from '../../Contexts';
5 |
6 | export function FriendCard({fetchProfile, profile, request = false}) {
7 | const lang = useContext(LangContext);
8 | const [deleted, setDeleted] = useState(false);
9 |
10 | function handleClick(call) {
11 | fetch(`/api/friends/${call}/${profile.name}`, {
12 | method: "POST",
13 | })
14 | .then((res) => {
15 | if (!res.ok) throw new Error(res.statusText);
16 | return res.json();
17 | })
18 | .then((data) => {
19 | fetchProfile();
20 | })
21 | .catch((err) => {});
22 | }
23 |
24 | const time = new Date();
25 | const isOnline =
26 | time.getTime() - new Date(profile.last_online).getTime() < 60000;
27 |
28 | return (
29 | <>
30 | {deleted ? null : (
31 |
32 |
33 |
34 |
37 |
38 |
39 | {profile.name}
40 |
41 |
42 | {language.level[lang]} {profile.level}
43 |
44 |
45 |
46 | {request ? (
47 |
48 | handleClick("accept")}
50 | className="aspect-square w-7 h-7 flex items-center justify-center rounded-full border hover:border-green-600 border-green-500"
51 | >
52 |
53 |
54 | handleClick("deny")}
56 | className="aspect-square w-7 h-7 flex items-center justify-center rounded-full border hover:border-red-600 border-red-500 ml-2"
57 | >
58 |
59 |
60 |
61 | ) : (
62 |
handleClick("remove")}>
63 |
64 |
65 | )}
66 |
67 |
68 | )}
69 | >
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/frontend/src/pages/Games/Pong.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react';
2 | import { useLocation } from 'preact-iso';
3 | import { Card } from '../../components/utils/Card';
4 | import { CTA } from '../../components/utils/CTA';
5 | import { Chat } from '../../components/Chat/Chat';
6 | import { language } from '../../scripts/languages';
7 | import { ProfileContext, LangContext } from '../../Contexts';
8 | import init, { start } from 'pong-client';
9 |
10 | export function Pong() {
11 | const [mode, setMode] = useState("bot");
12 | const loc = useLocation();
13 | const profile = useContext(ProfileContext);
14 | const lang = useContext(LangContext);
15 |
16 | useEffect(() => init().then(start), []);
17 |
18 | return (
19 |
20 |
24 | 4 - 1
25 |
26 |
50 |
56 |
76 |
82 | Pong
83 |
84 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/frontend/pong-client/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 | sudo: false
3 |
4 | cache: cargo
5 |
6 | matrix:
7 | include:
8 |
9 | # Builds with wasm-pack.
10 | - rust: beta
11 | env: RUST_BACKTRACE=1
12 | addons:
13 | firefox: latest
14 | chrome: stable
15 | before_script:
16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
18 | - cargo install-update -a
19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
20 | script:
21 | - cargo generate --git . --name testing
22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere
23 | # in any of our parent dirs is problematic.
24 | - mv Cargo.toml Cargo.toml.tmpl
25 | - cd testing
26 | - wasm-pack build
27 | - wasm-pack test --chrome --firefox --headless
28 |
29 | # Builds on nightly.
30 | - rust: nightly
31 | env: RUST_BACKTRACE=1
32 | before_script:
33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
35 | - cargo install-update -a
36 | - rustup target add wasm32-unknown-unknown
37 | script:
38 | - cargo generate --git . --name testing
39 | - mv Cargo.toml Cargo.toml.tmpl
40 | - cd testing
41 | - cargo check
42 | - cargo check --target wasm32-unknown-unknown
43 | - cargo check --no-default-features
44 | - cargo check --target wasm32-unknown-unknown --no-default-features
45 | - cargo check --no-default-features --features console_error_panic_hook
46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc"
48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc"
49 |
50 | # Builds on beta.
51 | - rust: beta
52 | env: RUST_BACKTRACE=1
53 | before_script:
54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
56 | - cargo install-update -a
57 | - rustup target add wasm32-unknown-unknown
58 | script:
59 | - cargo generate --git . --name testing
60 | - mv Cargo.toml Cargo.toml.tmpl
61 | - cd testing
62 | - cargo check
63 | - cargo check --target wasm32-unknown-unknown
64 | - cargo check --no-default-features
65 | - cargo check --target wasm32-unknown-unknown --no-default-features
66 | - cargo check --no-default-features --features console_error_panic_hook
67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
68 | # Note: no enabling the `wee_alloc` feature here because it requires
69 | # nightly for now.
70 |
--------------------------------------------------------------------------------
/frontend/src/pages/Shop/index.jsx:
--------------------------------------------------------------------------------
1 | import {ShopItem} from '../../components/Shop/ShopItem';
2 | import { useContext, useEffect, useState } from 'preact/hooks';
3 | import { ProfileContext, LangContext } from '../../Contexts';
4 |
5 | export function Shop({ fetchProfile }) {
6 | const [page, setPage] = useState(0);
7 | const [items, setItems] = useState(null);
8 |
9 | const [factor, setFactor] = useState(window.screen.width < 768 ? 8 : 12);
10 |
11 | useEffect(() => {
12 | if (items == null) {
13 | fetch("/api/shop").then((res) =>
14 | res.json().then((data) => {
15 | setItems(data.items);
16 | }),
17 | );
18 | }
19 |
20 | window.addEventListener("resize", () => {
21 | setFactor(window.screen.width < 768 ? 8 : 12);
22 | });
23 | return () => {
24 | window.removeEventListener("resize", () => {
25 | setFactor(window.screen.width < 768 ? 8 : 12);
26 | });
27 | };
28 | }, []);
29 |
30 | const profile = useContext(ProfileContext);
31 | const lang = useContext(LangContext);
32 |
33 | return (
34 |
37 |
38 | {items
39 | ? items
40 | .slice(page * factor, (page + 1) * factor)
41 | .map((item, idx) => (
42 |
52 | ))
53 | : null}
54 |
55 | {items ? (
56 |
57 |
{
59 | setPage(page > 0 ? page - 1 : page);
60 | }}
61 | className={`w-6 ${page <= 0 ? "fill-white/40 hover:cursor-auto" : "fill-white/60 cursor-pointer hover:fill-white"}`}
62 | xmlns="http://www.w3.org/2000/svg"
63 | viewBox="0 0 256 512"
64 | >
65 |
66 |
67 |
{page + 1}
68 |
{
70 | setPage(page + 1 < items.length / factor ? page + 1 : page);
71 | }}
72 | className={`w-6 ${page + 1 >= items.length / factor ? "fill-white/40 hover:cursor-auto" : "fill-white/60 cursor-pointer hover:fill-white"}`}
73 | xmlns="http://www.w3.org/2000/svg"
74 | viewBox="0 0 256 512"
75 | >
76 |
77 |
78 |
79 | ) : null}
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/src/pages/Games/Bonk.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useEffect } from "react";
2 | import { Card } from "../../components/utils/Card";
3 | import { CTA } from "../../components/utils/CTA";
4 | import { Chat } from "../../components/Chat/Chat";
5 | import { language } from "../../scripts/languages";
6 | import { LangContext } from "../../Contexts";
7 | import { ProfileContext } from "../../Contexts";
8 |
9 | export function Bonk(props) {
10 | const [popUp, setPopUp] = useState(true);
11 | const [playClicked, setPlayClicked] = useState(false);
12 |
13 | const lang = useContext(LangContext);
14 | const profile = useContext(ProfileContext);
15 |
16 | window.godotFunctions = {};
17 | window.externalator = {
18 | addGodotFunction: (n, f) => {
19 | window.godotFunctions[n] = f;
20 | },
21 | };
22 |
23 | if (!profile?.name) props.fetchProfile();
24 |
25 | useEffect(() => {
26 | const loadScripts = async () => {
27 | try {
28 | await loadScript("/static/pako_inflate.min.js");
29 | await loadScript("/static/bonk-client.js");
30 | await loadScript("/static/status.js");
31 | } catch (error) {
32 | console.error("Error loading scripts:", error);
33 | }
34 | };
35 |
36 | loadScripts();
37 | }, []);
38 |
39 | useEffect(() => {
40 | if (profile.playing && !playClicked) setPlayClicked(true);
41 | }, [profile]);
42 |
43 | const loadScript = (src) => {
44 | return new Promise((resolve, reject) => {
45 | const script = document.createElement("script");
46 | script.src = src;
47 | script.onload = resolve;
48 | script.onerror = reject;
49 | document.body.appendChild(script);
50 | });
51 | };
52 |
53 | const handlePlayClick = () => {
54 | godotFunctions.join_request();
55 | setPopUp(false);
56 | setPlayClicked(true);
57 | };
58 |
59 | return (
60 | <>
61 | {popUp && !profile?.playing ? (
62 |
69 | ) : null}
70 |
71 | HTML5 canvas appears to be unsupported in the current browser. Please
72 | try updating or use a different browser.
73 |
74 |
75 |
e.preventDefault()}
79 | >
80 |
81 |
82 |
e.preventDefault()}
86 | >
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
101 |
102 |
103 | >
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/frontend/src/pages/Auth/Login.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "../../components/utils/Card";
3 | import { Input } from "../../components/utils/Input";
4 | import { CTA } from "../../components/utils/CTA";
5 | import { useContext, useEffect, useRef, useState } from "preact/hooks";
6 | import { useLocation } from "preact-iso";
7 | import { language } from "../../scripts/languages";
8 | import { LangContext } from "../../Contexts";
9 |
10 | const CLIENT_ID = document.querySelector("setting[name=CLIENT_ID]").textContent;
11 | const HOST = document.querySelector("setting[name=HOST]").textContent;
12 | document.querySelector("settings").remove();
13 |
14 | export function Login(props) {
15 | const lang = useContext(LangContext);
16 |
17 | const redirectUri = encodeURIComponent(`${location.protocol}//${HOST}`);
18 | const url = `https://api.intra.42.fr/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${redirectUri}/auth/42&response_type=code`;
19 |
20 | const username = useRef(null);
21 | const password = useRef(null);
22 |
23 | const [error, setError] = useState("");
24 |
25 | function handleLogin() {
26 | props.setTriedLog(true);
27 | fetch("/auth/login", {
28 | method: "POST",
29 | headers: {
30 | "Content-Type": "application/json",
31 | },
32 | body: JSON.stringify({
33 | username: username.current.value,
34 | password: password.current.value,
35 | }),
36 | })
37 | .then((res) => {
38 | if (!res.ok) {
39 | return res.json().then((errData) => {
40 | throw new Error(errData.error);
41 | });
42 | }
43 | })
44 | .then((data) => {
45 | window.location.pathname = "/play";
46 | })
47 | .catch((err) => {
48 | setError(err.message);
49 | username.current.value = "";
50 | password.current.value = "";
51 | });
52 | }
53 |
54 | function enter(e) {
55 | if (e.key == "Enter") handleLogin();
56 | }
57 |
58 | return (
59 |
60 |
89 |
90 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/backend/bonk-server/player/player.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=7 format=3 uid="uid://ecq8tbk7vkc0"]
2 |
3 | [ext_resource type="Script" path="res://player/body.gd" id="1_w7ysv"]
4 | [ext_resource type="Texture2D" uid="uid://ir5g2j7r5tkt" path="res://asset/circle.svg" id="2_2it3p"]
5 | [ext_resource type="FontFile" uid="uid://bwoq7xiu7yhey" path="res://asset/Ubuntu-Regular.ttf" id="3_334n7"]
6 | [ext_resource type="Script" path="res://player/grapple.gd" id="3_i2gif"]
7 |
8 | [sub_resource type="CircleShape2D" id="CircleShape2D_o1o6r"]
9 | radius = 30.0
10 |
11 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_fpr0t"]
12 | properties/0/path = NodePath("body:position")
13 | properties/0/spawn = true
14 | properties/0/replication_mode = 1
15 | properties/1/path = NodePath("body/power:tint_progress")
16 | properties/1/spawn = true
17 | properties/1/replication_mode = 1
18 | properties/2/path = NodePath("body/power:value")
19 | properties/2/spawn = true
20 | properties/2/replication_mode = 1
21 | properties/3/path = NodePath("grapple:points")
22 | properties/3/spawn = true
23 | properties/3/replication_mode = 1
24 | properties/4/path = NodePath("grapple:visible")
25 | properties/4/spawn = true
26 | properties/4/replication_mode = 1
27 | properties/5/path = NodePath("body:too_far")
28 | properties/5/spawn = true
29 | properties/5/replication_mode = 1
30 | properties/6/path = NodePath("body/username:text")
31 | properties/6/spawn = true
32 | properties/6/replication_mode = 1
33 | properties/7/path = NodePath("body:skin")
34 | properties/7/spawn = true
35 | properties/7/replication_mode = 1
36 |
37 | [node name="player" type="Node2D"]
38 |
39 | [node name="body" type="RigidBody2D" parent="."]
40 | visibility_layer = 2
41 | z_index = 1
42 | position = Vector2(640, 235)
43 | can_sleep = false
44 | freeze = true
45 | max_contacts_reported = 10
46 | contact_monitor = true
47 | script = ExtResource("1_w7ysv")
48 |
49 | [node name="skin" type="Sprite2D" parent="body"]
50 | position = Vector2(0.500008, 1.21593e-05)
51 | scale = Vector2(0.119141, 0.117188)
52 |
53 | [node name="collision" type="CollisionShape2D" parent="body"]
54 | shape = SubResource("CircleShape2D_o1o6r")
55 |
56 | [node name="power" type="TextureProgressBar" parent="body"]
57 | process_mode = 4
58 | offset_left = -39.0
59 | offset_top = -39.0
60 | offset_right = 40.0
61 | offset_bottom = 39.0
62 | pivot_offset = Vector2(39, 39)
63 | value = 100.0
64 | rounded = true
65 | fill_mode = 4
66 | nine_patch_stretch = true
67 | texture_under = ExtResource("2_2it3p")
68 | texture_progress = ExtResource("2_2it3p")
69 | texture_progress_offset = Vector2(-0.37, 0)
70 | tint_under = Color(1, 1, 1, 0.258824)
71 | tint_progress = Color(1, 1, 1, 0.498039)
72 | metadata/_edit_use_anchors_ = true
73 |
74 | [node name="username" type="Label" parent="body"]
75 | offset_left = -62.0
76 | offset_top = 31.0
77 | offset_right = 63.0
78 | offset_bottom = 54.0
79 | theme_override_fonts/font = ExtResource("3_334n7")
80 | horizontal_alignment = 1
81 |
82 | [node name="grapple" type="Line2D" parent="."]
83 | modulate = Color(1, 1, 1, 0.196078)
84 | z_index = -1
85 | points = PackedVector2Array(0, 0, 0, 0)
86 | width = 3.0
87 | script = ExtResource("3_i2gif")
88 |
89 | [node name="synchronizer" type="MultiplayerSynchronizer" parent="."]
90 | replication_config = SubResource("SceneReplicationConfig_fpr0t")
91 | visibility_update_mode = 1
92 |
93 | [node name="skin" type="HTTPRequest" parent="."]
94 |
95 | [connection signal="body_entered" from="body" to="body" method="_on_body_entered"]
96 | [connection signal="request_completed" from="skin" to="body" method="skin_request_completed"]
97 |
--------------------------------------------------------------------------------
/backend/bonk-server/export_presets.cfg:
--------------------------------------------------------------------------------
1 | [preset.0]
2 |
3 | name="Web"
4 | platform="Web"
5 | runnable=true
6 | dedicated_server=false
7 | custom_features=""
8 | export_filter="all_resources"
9 | include_filter=""
10 | exclude_filter=""
11 | export_path="../bonk-web-export/bonk.html"
12 | encryption_include_filters=""
13 | encryption_exclude_filters=""
14 | encrypt_pck=false
15 | encrypt_directory=false
16 |
17 | [preset.0.options]
18 |
19 | custom_template/debug=""
20 | custom_template/release=""
21 | variant/extensions_support=false
22 | vram_texture_compression/for_desktop=true
23 | vram_texture_compression/for_mobile=false
24 | html/export_icon=true
25 | html/custom_html_shell=""
26 | html/head_include=""
27 | html/canvas_resize_policy=2
28 | html/focus_canvas_on_start=true
29 | html/experimental_virtual_keyboard=false
30 | progressive_web_app/enabled=false
31 | progressive_web_app/offline_page=""
32 | progressive_web_app/display=1
33 | progressive_web_app/orientation=0
34 | progressive_web_app/icon_144x144=""
35 | progressive_web_app/icon_180x180=""
36 | progressive_web_app/icon_512x512=""
37 | progressive_web_app/background_color=Color(0, 0, 0, 1)
38 | include_coi_service_worker=true
39 | iframe_breakout="Same Tab"
40 |
41 | [preset.1]
42 |
43 | name="client"
44 | platform="Linux/X11"
45 | runnable=true
46 | dedicated_server=false
47 | custom_features=""
48 | export_filter="all_resources"
49 | include_filter=""
50 | exclude_filter=""
51 | export_path="../bonk-export/bonk.x86_64"
52 | encryption_include_filters=""
53 | encryption_exclude_filters=""
54 | encrypt_pck=false
55 | encrypt_directory=false
56 |
57 | [preset.1.options]
58 |
59 | custom_template/debug=""
60 | custom_template/release=""
61 | debug/export_console_wrapper=1
62 | binary_format/embed_pck=false
63 | texture_format/bptc=true
64 | texture_format/s3tc=true
65 | texture_format/etc=false
66 | texture_format/etc2=false
67 | binary_format/architecture="x86_64"
68 | ssh_remote_deploy/enabled=false
69 | ssh_remote_deploy/host="user@host_ip"
70 | ssh_remote_deploy/port="22"
71 | ssh_remote_deploy/extra_args_ssh=""
72 | ssh_remote_deploy/extra_args_scp=""
73 | ssh_remote_deploy/run_script="#!/usr/bin/env bash
74 | export DISPLAY=:0
75 | unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
76 | \"{temp_dir}/{exe_name}\" {cmd_args}"
77 | ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
78 | kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
79 | rm -rf \"{temp_dir}\""
80 |
81 | [preset.2]
82 |
83 | name="server"
84 | platform="Linux/X11"
85 | runnable=false
86 | dedicated_server=true
87 | custom_features="server"
88 | export_filter="customized"
89 | customized_files={
90 | "res://": "strip"
91 | }
92 | include_filter=""
93 | exclude_filter=""
94 | export_path="../bonk-server-export/bonk-server.x86_64"
95 | encryption_include_filters=""
96 | encryption_exclude_filters=""
97 | encrypt_pck=false
98 | encrypt_directory=false
99 |
100 | [preset.2.options]
101 |
102 | custom_template/debug=""
103 | custom_template/release=""
104 | debug/export_console_wrapper=1
105 | binary_format/embed_pck=false
106 | texture_format/bptc=true
107 | texture_format/s3tc=true
108 | texture_format/etc=false
109 | texture_format/etc2=false
110 | binary_format/architecture="x86_64"
111 | ssh_remote_deploy/enabled=false
112 | ssh_remote_deploy/host="user@host_ip"
113 | ssh_remote_deploy/port="22"
114 | ssh_remote_deploy/extra_args_ssh=""
115 | ssh_remote_deploy/extra_args_scp=""
116 | ssh_remote_deploy/run_script="#!/usr/bin/env bash
117 | export DISPLAY=:0
118 | unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
119 | \"{temp_dir}/{exe_name}\" {cmd_args}"
120 | ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
121 | kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
122 | rm -rf \"{temp_dir}\""
123 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile/FriendList.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect, useContext } from "react";
2 | import { FriendCard } from "./FriendCard";
3 | import { PopUp } from "../utils/PopUp";
4 | import { Input } from "../utils/Input";
5 | import { CTA } from "../utils/CTA";
6 | import { Card } from "../utils/Card";
7 | import { language } from "../../scripts/languages";
8 | import { LangContext } from "../../Contexts";
9 |
10 | export function FriendList({ fetchProfile, friends, friendsRequests }) {
11 | const lang = useContext(LangContext);
12 |
13 | const [popUp, setPopUp] = useState(false);
14 | const [error, setError] = useState("");
15 | const inputRef = useRef(null);
16 | const [loaded, setLoaded] = useState(false);
17 |
18 | useEffect(() => {
19 | setLoaded(true);
20 | }, []);
21 |
22 | function handleClick() {
23 | if (inputRef.current.value.trim().length) {
24 | fetch(`/api/friends/send/${inputRef.current.value}`, {
25 | method: "POST",
26 | })
27 | .then((res) => {
28 | if (!res.ok) {
29 | return res.json().then((errData) => {
30 | throw new Error(errData.error);
31 | });
32 | }
33 | fetchProfile();
34 | })
35 | .then((data) => {
36 | setPopUp(false);
37 | })
38 | .catch((err) => {
39 | setError(err.message);
40 | });
41 | }
42 | inputRef.current.value = "";
43 | }
44 |
45 | function clear() {
46 | inputRef.current.value = "";
47 | setError("");
48 | }
49 |
50 | function enter(e) {
51 | if (e.key == "Enter") handleClick();
52 | }
53 |
54 | return (
55 |
56 |
63 | {language.add_friend[lang]}
64 |
69 | {error}
70 |
71 | {language.invite[lang]}
72 |
73 |
74 |
75 |
76 |
77 | {language.friends[lang]}
78 |
79 | {
81 | setPopUp(true);
82 | }}
83 | className="fa-solid fa-circle-plus mr-2 hover:text-gray-300 cursor-pointer"
84 | >
85 |
86 |
89 | {friendsRequests.map((friend) => (
90 |
95 | ))}
96 | {friends.map((friend) => (
97 |
98 | ))}
99 | {!friends.length && !friendsRequests.length ? (
100 |
101 |
102 | {language.no_friends[lang]}
103 |
104 |
105 | ) : null}
106 |
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/backend/ponk/chat.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from asgiref.sync import async_to_sync
3 | from channels.generic.websocket import WebsocketConsumer
4 | from datetime import timedelta
5 | from django.utils import timezone
6 | import json
7 |
8 |
9 | MAIN_ROOM = "Lobby"
10 |
11 |
12 | class Chat(WebsocketConsumer):
13 | def connect(self):
14 | self.accept()
15 | self.user = self.scope["user"]
16 |
17 | if self.user.is_authenticated:
18 | async_to_sync(self.channel_layer.group_add)(MAIN_ROOM, self.channel_name)
19 |
20 | for friend in self.user.friends.all():
21 | users = sorted([friend.username, self.user.username])
22 | room_name = f"{users[0]}_{users[1]}"
23 | async_to_sync(self.channel_layer.group_add)(
24 | room_name, self.channel_name
25 | )
26 |
27 | self.send(
28 | json.dumps(
29 | dict(
30 | type="friends",
31 | friends=list(
32 | map(
33 | lambda f: f.username,
34 | filter(
35 | lambda f: timezone.now() - f.last_online
36 | < timedelta(seconds=10),
37 | self.user.friends.all(),
38 | ),
39 | )
40 | ),
41 | )
42 | )
43 | )
44 |
45 | def disconnect(self, _):
46 | if self.user.is_authenticated:
47 | async_to_sync(self.channel_layer.group_discard)(
48 | MAIN_ROOM, self.channel_name
49 | )
50 |
51 | for friend in self.user.friends.all():
52 | users = sorted([friend.username, self.user.username])
53 | room_name = f"{users[0]}_{users[1]}"
54 | async_to_sync(self.channel_layer.group_add)(
55 | room_name, self.channel_name
56 | )
57 |
58 | def receive(self, text_data):
59 | message = json.loads(text_data)
60 | if "content" in message and "room" in message:
61 | message["author"] = self.user.username
62 | message["avatar"] = self.user.avatar
63 | message["level"] = self.user.level
64 |
65 | if (
66 | message["content"].strip().startswith("/invite ")
67 | and message["room"] == "Lobby"
68 | ):
69 | dest = message["content"].strip()[len("/invite ") :]
70 | users = sorted([dest, self.user.username])
71 | message["room"] = f"{users[0]}_{users[1]}"
72 | message["content"] = "I invite you to join my game!"
73 |
74 | async_to_sync(self.channel_layer.group_send)(
75 | message["room"], {"type": "chat.message", "message": message}
76 | )
77 |
78 | def chat_message(self, event):
79 | message = event["message"]
80 |
81 | if message["content"].strip() == "":
82 | return
83 |
84 | # ew ew ew ew ew ew
85 | if "_" in message["room"]:
86 | user1, user2 = message["room"].split("_")
87 | if user1 != self.user.username:
88 | real_room = user1
89 | else:
90 | real_room = user2
91 | else:
92 | real_room = MAIN_ROOM
93 |
94 | message["room"] = real_room
95 | if message["author"] != self.user.username:
96 | message["type"] = "message"
97 | self.send(text_data=json.dumps(message))
98 |
99 |
100 | urls = [
101 | path("chat", Chat.as_asgi()),
102 | ]
103 |
--------------------------------------------------------------------------------
/frontend/src/index.jsx:
--------------------------------------------------------------------------------
1 | import "vite/modulepreload-polyfill";
2 | import "./style.css";
3 | import { render } from "preact";
4 | import { LocationProvider, Router } from "preact-iso";
5 | import { Navbar } from "./components/Navbar/Navbar.jsx";
6 | import { Play } from "./pages/Play/index.jsx";
7 | import { Room } from "./pages/Tournament/Room.jsx";
8 | import { Menu } from "./pages/Tournament/Menu.jsx";
9 | import { Shop } from "./pages/Shop/index.jsx";
10 | import { Login } from "./pages/Auth/Login.jsx";
11 | import { NotFound } from "./pages/_404.jsx";
12 | import { BVAmbient } from "./scripts/bvambient.js";
13 | import { Profile } from "./pages/Profile/index.jsx";
14 | import { Signup } from "./pages/Auth/Signup";
15 | import { Pong } from "./pages/Games/Pong";
16 | import { Bonk } from "./pages/Games/Bonk";
17 | import { useEffect, useState } from "preact/hooks";
18 | import { LegalNotice } from "./pages/LegalNotice/LegalNotice";
19 | import { ProfileContext, LangContext } from "./Contexts";
20 | import { Waiting } from "./pages/Tournament/Waiting";
21 |
22 | export function App() {
23 | const [profile, setProfile] = useState(null);
24 | const [triedLog, setTriedLog] = useState(false);
25 | const [lang, setLang] = useState("en");
26 |
27 | function fetchProfile() {
28 | fetch("/api/me").then((res) =>
29 | res.json().then((data) => {
30 | setProfile(data);
31 | setTriedLog(true);
32 | }),
33 | );
34 | }
35 |
36 | useEffect(() => {
37 | if (profile == null) {
38 | fetchProfile();
39 | }
40 |
41 | new BVAmbient({
42 | selector: "#ambient",
43 | fps: 60,
44 | max_transition_speed: 20000,
45 | min_transition_speed: 10000,
46 | particle_number: 100,
47 | particle_maxwidth: 30,
48 | particle_minwidth: 10,
49 | particle_radius: 50,
50 | particle_opacity: true,
51 | particle_colision_change: false,
52 | particle_background: "#331e4b",
53 | refresh_onfocus: false,
54 | });
55 |
56 | fetch("/api/ping");
57 | const intervalId = setInterval(() => {
58 | fetch("/api/ping");
59 | }, 5000);
60 | return () => clearInterval(intervalId);
61 | }, []);
62 |
63 | return (
64 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
100 | render( , document.getElementById("app"));
101 |
--------------------------------------------------------------------------------
/backend/ponk/private.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from ponk.models import GameHistory, User
3 | from django.http.response import JsonResponse
4 | from django.views.decorators.csrf import csrf_exempt
5 | from ponk.api_decorators import private_api_auth
6 | from ponk.tournament import game_ended
7 | from ponk.tournament import del_room
8 | from ponk.tournament import rooms
9 | from ponk.utils import get_selected_skin_url
10 | import json
11 | import sys
12 | import os
13 | import secrets
14 |
15 |
16 | events = []
17 | tokens = {}
18 |
19 |
20 | @csrf_exempt
21 | @private_api_auth
22 | def game_stats(request, *args, **kwargs):
23 | try:
24 | data = json.loads(request.body)
25 | score_user = data["score"][0]
26 | score_opponent = data["score"][1]
27 |
28 | win = score_user > score_opponent
29 | multiplier = 10 if win else 1
30 |
31 | if score_user == 4 and score_opponent == 1:
32 | multiplier = 17.25
33 |
34 | try:
35 | xp = (
36 | max(score_user, score_opponent) / min(score_user, score_opponent)
37 | ) * multiplier
38 | except ZeroDivisionError:
39 | xp = (max(score_user, score_opponent) / 1) * multiplier + 5
40 |
41 | user = User.objects.get(username=data["player"])
42 | user.level_percentage += xp
43 | if user.level_percentage >= 100:
44 | user.level += user.level_percentage // 100
45 | user.level_percentage = user.level_percentage % 100
46 | user.save()
47 |
48 | del_room(user)
49 |
50 | if win and user in rooms:
51 | game_ended(user)
52 |
53 | GameHistory(
54 | user=user, game=data["game"], score=data["score"], win=win, xp=xp
55 | ).save()
56 |
57 | return JsonResponse(
58 | {
59 | "success": True,
60 | }
61 | )
62 | except BaseException as e:
63 | print(e, file=sys.stderr)
64 | return JsonResponse(
65 | {
66 | "error": "missing fields!!!!",
67 | },
68 | status=400,
69 | )
70 |
71 |
72 | @csrf_exempt
73 | @private_api_auth
74 | def get_bonk_events(request, *args, **kwargs):
75 | if len(events):
76 | return JsonResponse(
77 | events.pop(0),
78 | )
79 | return JsonResponse(
80 | {
81 | "error": "no events",
82 | },
83 | status=400,
84 | )
85 |
86 |
87 | @csrf_exempt
88 | @private_api_auth
89 | def set_bonk_events(request, *args, **kwargs):
90 | try:
91 | data = json.loads(request.body)
92 |
93 | events.append({"game_id": data["game_id"], "users": data["users"]})
94 |
95 | return JsonResponse(
96 | {
97 | "success": True,
98 | },
99 | )
100 | except BaseException as e:
101 | print(e, file=sys.stderr)
102 | return JsonResponse(
103 | {
104 | "error": "missing fields!!!!",
105 | },
106 | status=400,
107 | )
108 |
109 |
110 | @csrf_exempt
111 | @private_api_auth
112 | def get_player_token(request, *args, **kwargs):
113 | try:
114 | token = args[1].get("token")
115 | username = {value: key for key, value in tokens.items()}.get(token)
116 | user = User.objects.get(username=username)
117 | return JsonResponse(
118 | {
119 | "name": user.username,
120 | "skin": get_selected_skin_url(user.username),
121 | }
122 | )
123 | except BaseException as e:
124 | print(e, file=sys.stderr)
125 | return JsonResponse(
126 | {
127 | "error": "Invalid token",
128 | },
129 | status=400,
130 | )
131 |
132 |
133 | urls = [
134 | path("game_stats", game_stats),
135 | path("get_bonk_event", get_bonk_events),
136 | path("set_bonk_event", set_bonk_events),
137 | path("get_bonk_player/", get_player_token),
138 | ]
139 |
--------------------------------------------------------------------------------
/backend/bonk-server/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=5
10 |
11 | [application]
12 |
13 | config/name="bonk"
14 | run/main_scene="res://main.tscn"
15 | config/features=PackedStringArray("4.2", "GL Compatibility")
16 | config/icon="res://asset/icon.svg"
17 |
18 | [autoload]
19 |
20 | Global="*res://global.gd"
21 | Settings="*res://settings.gd"
22 |
23 | [display]
24 |
25 | window/size/viewport_width=1280
26 | window/size/viewport_height=720
27 | window/stretch/mode="canvas_items"
28 | window/stretch/aspect="expand"
29 |
30 | [dotnet]
31 |
32 | project/assembly_name="bonk"
33 |
34 | [editor]
35 |
36 | export/convert_text_resources_to_binary=false
37 |
38 | [input]
39 |
40 | up={
41 | "deadzone": 0.5,
42 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
43 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
44 | ]
45 | }
46 | down={
47 | "deadzone": 0.5,
48 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
49 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"echo":false,"script":null)
50 | ]
51 | }
52 | left={
53 | "deadzone": 0.5,
54 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null)
55 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"echo":false,"script":null)
56 | ]
57 | }
58 | right={
59 | "deadzone": 0.5,
60 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
61 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
62 | ]
63 | }
64 | grapple={
65 | "deadzone": 0.5,
66 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":0,"echo":false,"script":null)
67 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":0,"echo":false,"script":null)
68 | ]
69 | }
70 | heavy={
71 | "deadzone": 0.5,
72 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"echo":false,"script":null)
73 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":88,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
74 | ]
75 | }
76 |
77 | [rendering]
78 |
79 | renderer/rendering_method="mobile"
80 | renderer/rendering_method.mobile="gl_compatibility"
81 | environment/defaults/default_clear_color=Color(0.254902, 0.352941, 0.466667, 1)
82 |
--------------------------------------------------------------------------------
/frontend/src/scripts/languages.js:
--------------------------------------------------------------------------------
1 | export const language = {
2 | not_found: {
3 | en: "It's gone :(",
4 | fr: "Tu es perdu :(",
5 | br: "Kollet out-te :(",
6 | },
7 | create_room: {
8 | en: "Create a room",
9 | fr: "Creer une room",
10 | br: "Krouiñ ur sal",
11 | },
12 | join_room: {
13 | en: "Join a room",
14 | fr: "Rejoindre une room",
15 | br: "Mont d'ur sal",
16 | },
17 | search_someone: {
18 | en: "Search someone...",
19 | fr: "Recherchez quelqu'un...",
20 | br: "Klask unan bennak...",
21 | },
22 | add_user: {
23 | en: "Add an user",
24 | fr: "Ajoutez un joueur",
25 | br: "Ouzhpennañ ur c'hoarier",
26 | },
27 | invite: {
28 | en: "Invite",
29 | fr: "Inviter",
30 | br: "Pediñ",
31 | },
32 | play: {
33 | en: "Play",
34 | fr: "Jouer",
35 | br: "C'hoari",
36 | },
37 | profile: {
38 | en: "Profile",
39 | fr: "Profil",
40 | br: "Profil",
41 | },
42 | citation: {
43 | en: "Something you want to say...",
44 | fr: "Exprimez-vous...",
45 | br: "Un dra bennak 'fell dit lâr...",
46 | },
47 | new_password: {
48 | en: "New password",
49 | fr: "Nouveau mot de passe",
50 | br: "Ger-tremen nevez",
51 | },
52 | confirm_password: {
53 | en: "Confirm password",
54 | fr: "Confirmer le mot de passe",
55 | br: "Kadarnaat ar ger-tremen",
56 | },
57 | update: {
58 | en: "Update",
59 | fr: "Modifier",
60 | br: "Kemmañ",
61 | },
62 | log_out: {
63 | en: "Log out",
64 | fr: "Deconnexion",
65 | br: "Digevreadenn",
66 | },
67 | delete_account: {
68 | en: "Delete Account",
69 | fr: "Suppression",
70 | br: "O tilemel",
71 | },
72 | level: {
73 | en: "Level",
74 | fr: "Niveau",
75 | br: "Live",
76 | },
77 | friends: {
78 | en: "Friends",
79 | fr: "Amis",
80 | br: "Mignoned",
81 | },
82 | add_friend: {
83 | en: "Add a friend",
84 | fr: "Ajoutez un ami",
85 | br: " Ouzhpennañ ur mignon",
86 | },
87 | no_friends: {
88 | en: "No friends yet. Click on + to add a friend.",
89 | fr: "Aucun ami. Cliquez sur + pour ajouter un ami.",
90 | br: "Mignon ebet. Klikit war + da ouzhpennañ ur mignon.",
91 | },
92 | game_history: {
93 | en: "Game History",
94 | fr: "Historique de parties",
95 | br: "Istor al lodennoù",
96 | },
97 | game_history_default: {
98 | en: "No games yet.",
99 | fr: "Aucune partie.",
100 | br: "Lodenn ebet.",
101 | },
102 | play_against: {
103 | en: "Play against",
104 | fr: "Jouer contre",
105 | br: "C'hoari a-enep",
106 | },
107 | bot: {
108 | en: "Bot",
109 | fr: "Robot",
110 | br: "Emgefre",
111 | },
112 | player: {
113 | en: "Player",
114 | fr: "Joueur",
115 | br: "C'hoarier",
116 | },
117 | log_in: {
118 | en: "Log in",
119 | fr: "Connexion",
120 | br: "Kevreadenn",
121 | },
122 | log_in_42: {
123 | en: "Log in with 42",
124 | fr: "Connexion avec 42",
125 | br: "Kevreañ gant 42",
126 | },
127 | no_account: {
128 | en: "No account yet ?",
129 | fr: "Pas encore inscrit ?",
130 | br: "N'eo ket enskrivet c'hoazh?",
131 | },
132 | sign_up: {
133 | en: "Sign up",
134 | fr: "Inscription",
135 | br: "Enskrivadur",
136 | },
137 | username: {
138 | en: "Username",
139 | fr: "Pseudo",
140 | br: "Lesanv",
141 | },
142 | password: {
143 | en: "Password",
144 | fr: "Mot de passe",
145 | br: "Ger-tremen",
146 | },
147 | already_account: {
148 | en: "Already have an account ?",
149 | fr: "Déjà inscrit ?",
150 | br: "Enskrivet eo dija?",
151 | },
152 | players: {
153 | en: "players",
154 | fr: "joueurs",
155 | br: "c'hoarierien",
156 | },
157 | owned: {
158 | en: "Owned",
159 | fr: "Possédé",
160 | br: "Perc'hennet",
161 | },
162 | select: {
163 | en: "Select",
164 | fr: "Selectionner",
165 | br: "Dibab",
166 | },
167 | deselect: {
168 | en: "Deselect",
169 | fr: "Deselectionner",
170 | br: "Diziuzañ",
171 | },
172 | tournament: {
173 | en: "Tournament",
174 | fr: "Tournoi",
175 | br: " Tournamant",
176 | },
177 | shop: {
178 | en: "Shop",
179 | fr: "Boutique",
180 | br: "Stal",
181 | },
182 | legal_notice: {
183 | en: "Legal notice",
184 | fr: "Mentions Légales",
185 | br: "Menegoù lezennel",
186 | },
187 | your_message: {
188 | en: "Your message...",
189 | fr: "Votre message...",
190 | br: "Da gemennadenn...",
191 | },
192 | private: {
193 | en: "Private",
194 | fr: "Privé",
195 | br: "Prevez",
196 | },
197 | public: {
198 | en: "Public",
199 | fr: "Publique",
200 | br: "An holl",
201 | },
202 | no_tournament: {
203 | en: "No tournaments yet.",
204 | fr: "Aucun tournoi.",
205 | br: "Tournamant ebet.",
206 | },
207 | };
208 |
--------------------------------------------------------------------------------
/backend/bonk-server/map/map.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=8 format=3 uid="uid://3psjlgsy78cg"]
2 |
3 | [ext_resource type="Script" path="res://map/map.gd" id="1_cvoq4"]
4 | [ext_resource type="Script" path="res://map/collision.gd" id="2_6sgan"]
5 | [ext_resource type="Texture2D" uid="uid://byglgq480xi0k" path="res://asset/city.svg" id="3_xxrcf"]
6 | [ext_resource type="Script" path="res://map/rectangle.gd" id="4_ts5c7"]
7 | [ext_resource type="FontFile" uid="uid://bwoq7xiu7yhey" path="res://asset/Ubuntu-Regular.ttf" id="5_0xb43"]
8 |
9 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_5i30o"]
10 | size = Vector2(438.756, 38)
11 |
12 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_qcc0f"]
13 | properties/0/path = NodePath("scores:text")
14 | properties/0/spawn = true
15 | properties/0/replication_mode = 1
16 | properties/1/path = NodePath("message:text")
17 | properties/1/spawn = true
18 | properties/1/replication_mode = 1
19 |
20 | [node name="map" type="Node2D"]
21 | script = ExtResource("1_cvoq4")
22 |
23 | [node name="body" type="StaticBody2D" parent="."]
24 | z_index = -1
25 | position = Vector2(640, 320)
26 |
27 | [node name="area" type="Area2D" parent="body"]
28 | monitoring = false
29 | monitorable = false
30 |
31 | [node name="collisions" type="CollisionShape2D" parent="body/area"]
32 | shape = SubResource("RectangleShape2D_5i30o")
33 |
34 | [node name="collision" type="CollisionShape2D" parent="body"]
35 | shape = SubResource("RectangleShape2D_5i30o")
36 | script = ExtResource("2_6sgan")
37 |
38 | [node name="spawner" type="MultiplayerSpawner" parent="."]
39 | _spawnable_scenes = PackedStringArray("res://player/player.tscn")
40 | spawn_path = NodePath("../players")
41 |
42 | [node name="background" type="ParallaxBackground" parent="."]
43 |
44 | [node name="layer_1" type="ParallaxLayer" parent="background"]
45 | texture_repeat = 2
46 | position = Vector2(0, -57)
47 | motion_scale = Vector2(0.5, 0.5)
48 | motion_mirroring = Vector2(501.53, 0)
49 |
50 | [node name="city" type="Sprite2D" parent="background/layer_1"]
51 | modulate = Color(0.105882, 0.14902, 0.231373, 1)
52 | texture_repeat = 2
53 | position = Vector2(283.28, 587.878)
54 | scale = Vector2(0.426758, 0.364806)
55 | texture = ExtResource("3_xxrcf")
56 |
57 | [node name="city2" type="Sprite2D" parent="background/layer_1"]
58 | modulate = Color(0.105882, 0.14902, 0.231373, 1)
59 | texture_repeat = 2
60 | position = Vector2(782, 589)
61 | scale = Vector2(0.427, 0.365)
62 | texture = ExtResource("3_xxrcf")
63 |
64 | [node name="city3" type="Sprite2D" parent="background/layer_1"]
65 | modulate = Color(0.105882, 0.14902, 0.231373, 1)
66 | texture_repeat = 2
67 | position = Vector2(1284, 589)
68 | scale = Vector2(0.427, 0.365)
69 | texture = ExtResource("3_xxrcf")
70 |
71 | [node name="city4" type="Sprite2D" parent="background/layer_1"]
72 | modulate = Color(0.105882, 0.14902, 0.231373, 1)
73 | texture_repeat = 2
74 | position = Vector2(1784, 589)
75 | scale = Vector2(0.427, 0.365)
76 | texture = ExtResource("3_xxrcf")
77 |
78 | [node name="layer_2" type="ParallaxLayer" parent="background"]
79 | texture_repeat = 2
80 | motion_mirroring = Vector2(501.53, 0)
81 |
82 | [node name="city" type="Sprite2D" parent="background/layer_2"]
83 | modulate = Color(0.0509804, 0.109804, 0.160784, 1)
84 | texture_repeat = 2
85 | position = Vector2(283.28, 589.878)
86 | scale = Vector2(0.426758, 0.364806)
87 | texture = ExtResource("3_xxrcf")
88 |
89 | [node name="city2" type="Sprite2D" parent="background/layer_2"]
90 | modulate = Color(0.0509804, 0.109804, 0.160784, 1)
91 | texture_repeat = 2
92 | position = Vector2(782, 590)
93 | scale = Vector2(0.427, 0.365)
94 | texture = ExtResource("3_xxrcf")
95 |
96 | [node name="city3" type="Sprite2D" parent="background/layer_2"]
97 | modulate = Color(0.0509804, 0.109804, 0.160784, 1)
98 | texture_repeat = 2
99 | position = Vector2(1284, 590)
100 | scale = Vector2(0.427, 0.365)
101 | texture = ExtResource("3_xxrcf")
102 |
103 | [node name="city4" type="Sprite2D" parent="background/layer_2"]
104 | modulate = Color(0.0509804, 0.109804, 0.160784, 1)
105 | texture_repeat = 2
106 | position = Vector2(1784, 590)
107 | scale = Vector2(0.427, 0.365)
108 | texture = ExtResource("3_xxrcf")
109 |
110 | [node name="rectangle" type="Node2D" parent="."]
111 | position = Vector2(0, 719)
112 | script = ExtResource("4_ts5c7")
113 |
114 | [node name="scores" type="Label" parent="."]
115 | offset_left = 11.0
116 | offset_top = 10.0
117 | offset_right = 300.0
118 | offset_bottom = 166.0
119 | theme_override_fonts/font = ExtResource("5_0xb43")
120 | theme_override_font_sizes/font_size = 24
121 |
122 | [node name="synchronizer" type="MultiplayerSynchronizer" parent="."]
123 | replication_config = SubResource("SceneReplicationConfig_qcc0f")
124 |
125 | [node name="players" type="Node" parent="."]
126 |
127 | [node name="message" type="Label" parent="."]
128 | offset_top = 100.0
129 | offset_right = 1280.0
130 | offset_bottom = 200.0
131 | theme_override_fonts/font = ExtResource("5_0xb43")
132 | theme_override_font_sizes/font_size = 24
133 | horizontal_alignment = 1
134 |
--------------------------------------------------------------------------------
/frontend/src/components/Chat/Chat.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from "preact/hooks";
2 | import { Message } from "./Message";
3 | import { language } from "../../scripts/languages";
4 | import { createRef } from "preact";
5 | import { ProfileContext, LangContext } from "../../Contexts";
6 |
7 | function RoomButton({ withSeparator, children, activeRoom, ...rest }) {
8 | return (
9 | <>
10 | {withSeparator && | }
11 |
12 |
21 | {children}
22 |
23 |
24 | >
25 | );
26 | }
27 |
28 | let ws;
29 |
30 | export function Chat() {
31 | const profile = useContext(ProfileContext);
32 | const lang = useContext(LangContext);
33 |
34 | if (!profile) return;
35 |
36 | const ref = createRef();
37 |
38 | const me = profile.name;
39 | const [hover, setHover] = useState(false);
40 | const [shouldShowChat, setShouldShowChat] = useState(false);
41 | const [activeRoom, setActiveRoom] = useState("Lobby");
42 | const [friends, setFriends] = useState([]);
43 | const [messages, setMessages] = useState({});
44 |
45 | function sendChat(e) {
46 | e.preventDefault();
47 |
48 | const input = e.target.querySelector("input");
49 |
50 | if (ws) {
51 | const users = [me, activeRoom].sort();
52 | let actualRoomName = "Lobby";
53 | if (activeRoom != actualRoomName)
54 | actualRoomName = `${users[0]}_${users[1]}`;
55 | if (input.value.trim() == "") return;
56 |
57 | ws.send(
58 | JSON.stringify({
59 | room: actualRoomName,
60 | content: input.value,
61 | }),
62 | );
63 | (messages[activeRoom] ??= []).push(["local", { content: input.value }]);
64 | setMessages({ ...messages });
65 | input.value = "";
66 | }
67 | }
68 |
69 | useEffect(() => {
70 | ref?.current?.scrollTo(0, 100000);
71 | }, [messages, hover]);
72 |
73 | useEffect(() => {
74 | const url =
75 | (location.protocol == "https:" ? "wss:" : "ws:") +
76 | "//" +
77 | location.host +
78 | "/chat";
79 | ws = new WebSocket(url);
80 |
81 | ws.onopen = () => setShouldShowChat(true);
82 |
83 | ws.onmessage = ({ data }) => {
84 | const message = JSON.parse(data);
85 | switch (message.type) {
86 | case "friends":
87 | setFriends(message.friends);
88 | break;
89 | case "message":
90 | (messages[message.room] ??= []).push(["distant", message]);
91 | setMessages({...messages});
92 | break;
93 | }
94 | };
95 | }, []);
96 |
97 | const roomOpts = {
98 | activeRoom,
99 | onClick: (e) => setActiveRoom(e.target.textContent),
100 | };
101 |
102 | return (
103 | // Can't use hover: since it doesn't always trigger
104 | // in the same conditions as onmouseenter
105 | setHover(true)}
117 | onMouseLeave={() => setHover(false)}
118 | >
119 | {hover ? (
120 | <>
121 |
122 |
123 | Lobby
124 | {friends.map((friend) => (
125 |
126 | {friend}
127 |
128 | ))}
129 |
130 |
131 |
141 |
147 |
148 | >
149 | ) : (
150 |
153 | )}
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/backend/ponk/skins.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Division Alpha",
4 | "price": 4.2,
5 | "images": [
6 | "/assets/skins/cgodard.png",
7 | "/assets/skins/jcario.png",
8 | "/assets/skins/nlaerema.png"
9 | ]
10 | },
11 | {
12 | "name": "Division Omega",
13 | "price": 4.2,
14 | "images": [
15 | "/assets/skins/acasamit.png",
16 | "/assets/skins/vcornill.png",
17 | "/assets/skins/gprigent.png"
18 | ]
19 | },
20 | {
21 | "name": "Jesus",
22 | "price": 4.2,
23 | "images": [
24 | "/assets/skins/ndavenne.png",
25 | "/assets/skins/vcornill.png",
26 | "/assets/skins/aboyreau.png"
27 | ]
28 | },
29 | {
30 | "name": "Dinosaur",
31 | "price": 4.2,
32 | "images": [
33 | "/assets/skins/dinos.png",
34 | "/assets/skins/autre-dinos.png",
35 | "/assets/skins/encore-dinos.png"
36 | ]
37 | },
38 | {
39 | "name": "Division Alpha en chats",
40 | "price": 4.2,
41 | "images": [
42 | "/assets/skins/chat-yanne.png",
43 | "/assets/skins/dino-chat.png",
44 | "/assets/skins/chat-nils.png"
45 | ]
46 | },
47 | {
48 | "name": "Division Omega en chats",
49 | "price": 4.2,
50 | "images": [
51 | "/assets/skins/chat-antoine.png",
52 | "/assets/skins/chat-jesus.png",
53 | "/assets/skins/chat-chen.png"
54 | ]
55 | },
56 | {
57 | "name": "Aubry",
58 | "price": 4.2,
59 | "images": [
60 | "/assets/skins/laubry.png",
61 | "/assets/skins/kylian.png",
62 | "/assets/skins/kaubry.png"
63 | ]
64 | },
65 | {
66 | "name": "Dictionnaire",
67 | "price": 4.2,
68 | "images": [
69 | "/assets/skins/rriviere.png",
70 | "/assets/skins/atellier.png",
71 | "/assets/skins/rrouille.png"
72 | ]
73 | },
74 | {
75 | "name": "Louis",
76 | "price": 4.2,
77 | "images": [
78 | "/assets/skins/louis.png",
79 | "/assets/skins/lgalloux.png",
80 | "/assets/skins/autre-louis.png"
81 | ]
82 | },
83 | {
84 | "name": "Insu (Limited Edition)",
85 | "price": 4.2,
86 | "images": [
87 | "/assets/skins/insu-anan.png",
88 | "/assets/skins/insu-gwigwi.png",
89 | "/assets/skins/insu-juju.png"
90 | ]
91 | },
92 | {
93 | "name": "Poulet",
94 | "price": 4.2,
95 | "images": [
96 | "/assets/skins/poulet1.png",
97 | "/assets/skins/poulet2.png",
98 | "/assets/skins/poulet3.png"
99 | ]
100 | },
101 | {
102 | "name": "Ninjas",
103 | "price": 4.2,
104 | "images": [
105 | "/assets/skins/ryan-ninja.png",
106 | "/assets/skins/nils-ninja.png",
107 | "/assets/skins/ninja-effrayant.png"
108 | ]
109 | },
110 | {
111 | "name": "Petits Loustics",
112 | "price": 4.2,
113 | "images": [
114 | "assets/skins/loustic-principal.png",
115 | "assets/skins/louistic-2-mais-quand-meme-avec-le-loustic-principal.png",
116 | "assets/skins/loustic-3.png"
117 | ]
118 | },
119 | {
120 | "name": "Orcky",
121 | "price": 4.2,
122 | "images": [
123 | "/assets/skins/orcky.png",
124 | "/assets/skins/autre-orcky.png",
125 | "/assets/skins/encore-orcky.png"
126 | ]
127 | },
128 | {
129 | "name": "Mborde",
130 | "price": 4.2,
131 | "images": [
132 | "/assets/skins/mborde.png",
133 | "/assets/skins/maxborde.png",
134 | "/assets/skins/mbordeau.png"
135 | ]
136 | },
137 | {
138 | "name": "Tom Crous",
139 | "price": 4.2,
140 | "images": [
141 | "assets/skins/gwen-crous.png",
142 | "assets/skins/juju-crous.png",
143 | "assets/skins/tom-crous.png"
144 | ]
145 | },
146 | {
147 | "name": "Alexandru le beau",
148 | "price": 4.2,
149 | "images": [
150 | "assets/skins/alex-1.png",
151 | "assets/skins/alex-2.png",
152 | "assets/skins/alex-3.png"
153 | ]
154 | },
155 | {
156 | "name": "Alexandra",
157 | "price": 4.2,
158 | "images": [
159 | "assets/skins/alexa-1.png",
160 | "assets/skins/alexa-2.png",
161 | "assets/skins/alexa-3.png"
162 | ]
163 | },
164 | {
165 | "name": "Dorothée",
166 | "price": 4.2,
167 | "images": [
168 | "/assets/skins/dodo-1.png",
169 | "/assets/skins/dodo-2.png",
170 | "/assets/skins/dodo-3.png"
171 | ]
172 | },
173 | {
174 | "name": "Charlène",
175 | "price": 4.2,
176 | "images": [
177 | "/assets/skins/charchar-1.png",
178 | "/assets/skins/charchar-2.png",
179 | "/assets/skins/charchar-3.png"
180 | ]
181 | },
182 | {
183 | "name": "Monstres",
184 | "price": 4.2,
185 | "images": [
186 | "assets/skins/monstre-colere.png",
187 | "assets/skins/monstre-heureux.png",
188 | "assets/skins/monstre-triste.png"
189 | ]
190 | },
191 | {
192 | "name": "Contributions",
193 | "price": 4.2,
194 | "images": [
195 | "/assets/skins/chayanne-from-emma.png",
196 | "/assets/skins/ze_ouizarde.png",
197 | "/assets/skins/vader.png"
198 | ]
199 | },
200 | {
201 | "name": "Pote Antoine",
202 | "price": 4.2,
203 | "images": [
204 | "/assets/skins/pote-antoine.png",
205 | "/assets/skins/autre-pote-antoine.png",
206 | "/assets/skins/encore-pote-antoine.png"
207 | ]
208 | },
209 | {
210 | "name": "Our Users Data",
211 | "price": 4.2,
212 | "images": [
213 | "/assets/skins/cash.png",
214 | "/assets/skins/pointing-finger-at-you.png",
215 | "/assets/skins/data.png"
216 | ]
217 | }
218 | ]
219 |
--------------------------------------------------------------------------------
/backend/ponk/friends.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.http import JsonResponse
3 | from django.shortcuts import get_object_or_404
4 | from django.core.exceptions import ObjectDoesNotExist
5 | from ponk.api_decorators import authenticated
6 | from ponk.models import User
7 |
8 |
9 | @authenticated
10 | def friend_request(request, *args, **kwargs):
11 | username = args[1].get("user")
12 | try:
13 | target_user = User.objects.get(username=username)
14 | except ObjectDoesNotExist:
15 | return JsonResponse(
16 | {"error": "User {} does not exist".format(username)}, status=404
17 | )
18 |
19 | if username == request.user.username:
20 | return JsonResponse(
21 | {"error": "You cannot send a friend request to yourself"}, status=409
22 | )
23 |
24 | if request.user in target_user.friend_requests.all():
25 | return JsonResponse(
26 | {"error": "You have already sent a friend request to {}".format(username)},
27 | status=409,
28 | )
29 |
30 | if target_user in request.user.friends.all():
31 | return JsonResponse(
32 | {"error": "{} is already in your friend list".format(username)}, status=409
33 | )
34 |
35 | target_user.friend_requests.add(request.user)
36 | return JsonResponse({"success": True})
37 |
38 |
39 | @authenticated
40 | def accept_friend_request(request, *args, **kwargs):
41 | username = args[1].get("user")
42 | try:
43 | target_user = User.objects.get(username=username)
44 | except ObjectDoesNotExist:
45 | return JsonResponse(
46 | {"error": "User {} does not exist".format(username)}, status=404
47 | )
48 |
49 | if target_user not in request.user.friend_requests.all():
50 | return JsonResponse(
51 | {"error": "{} has not sent you a friend request".format(username)},
52 | status=404,
53 | )
54 |
55 | if target_user in request.user.friends.all():
56 | return JsonResponse(
57 | {"error": "{} is already in your friend list".format(username)}, status=409
58 | )
59 |
60 | request.user.friends.add(target_user)
61 | request.user.friend_requests.remove(target_user)
62 | target_user.friend_requests.remove(request.user)
63 | return JsonResponse({"success": True})
64 |
65 |
66 | @authenticated
67 | def deny_friend_request(request, *args, **kwargs):
68 | username = args[1].get("user")
69 | try:
70 | target_user = User.objects.get(username=username)
71 | except ObjectDoesNotExist:
72 | return JsonResponse(
73 | {"error": "User {} does not exist".format(username)}, status=404
74 | )
75 |
76 | if target_user not in request.user.friend_requests.all():
77 | return JsonResponse(
78 | {"error": "{} has not sent you a friend request".format(username)},
79 | status=404,
80 | )
81 |
82 | request.user.friend_requests.remove(target_user)
83 | return JsonResponse({"success": True})
84 |
85 |
86 | @authenticated
87 | def remove_friend(request, *args, **kwargs):
88 | username = args[1].get("user")
89 | try:
90 | target_user = User.objects.get(username=username)
91 | except ObjectDoesNotExist:
92 | return JsonResponse(
93 | {"error": "User {} does not exist".format(username)}, status=404
94 | )
95 |
96 | if target_user not in request.user.friends.all():
97 | return JsonResponse(
98 | {"error": "{} is not in your friend list".format(username)}, status=404
99 | )
100 |
101 | request.user.friends.remove(target_user)
102 | return JsonResponse({"success": True})
103 |
104 |
105 | def get_friends_info(username):
106 | try:
107 | user = User.objects.get(username=username)
108 | except User.DoesNotExist:
109 | return {"error": f"User {username} does not exist"}
110 |
111 | friends = user.friends.all()
112 | friends_info = []
113 |
114 | for friend in friends:
115 | friend_info = {
116 | "name": friend.username,
117 | "avatar": friend.avatar,
118 | "level": friend.level,
119 | "last_online": friend.last_online,
120 | }
121 | friends_info.append(friend_info)
122 |
123 | return friends_info
124 |
125 |
126 | def get_friends_request_info(username):
127 | try:
128 | user = User.objects.get(username=username)
129 | except User.DoesNotExist:
130 | return {"error": f"User {username} does not exist"}
131 |
132 | friends = user.friend_requests.all()
133 | friends_info = []
134 |
135 | for friend in friends:
136 | friend_info = {
137 | "name": friend.username,
138 | "avatar": friend.avatar,
139 | "level": friend.level,
140 | }
141 | friends_info.append(friend_info)
142 |
143 | return friends_info
144 |
145 |
146 | urls = [
147 | path("send/", friend_request),
148 | path("accept/", accept_friend_request),
149 | path("deny/", deny_friend_request),
150 | path("remove/", remove_friend),
151 | ]
152 |
--------------------------------------------------------------------------------
/frontend/src/pages/Tournament/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useRef, useState } from "react";
2 | import { CTA } from "../../components/utils/CTA";
3 | import { InvitationCard } from "../../components/Tournament/InvitationCard";
4 | import { language } from "../../scripts/languages";
5 | import { LangContext } from "../../Contexts";
6 | import { ProfileContext } from "../../Contexts";
7 | import { PopUp } from "../../components/utils/PopUp";
8 | import { useLocation } from "preact-iso";
9 | import { Input } from "../../components/utils/Input";
10 | import { Card } from "../../components/utils/Card";
11 |
12 | export function Menu(props) {
13 | const [popUp, setPopUp] = useState(false);
14 | const [error, setError] = useState("");
15 | const [errorJoin, setErrorJoin] = useState("");
16 | const location = useLocation();
17 |
18 | const [nbPlayers, setNbPlayers] = useState(8);
19 | const [tournaments, setTournaments] = useState([]);
20 |
21 | const nameRef = useRef(null);
22 |
23 | const lang = useContext(LangContext);
24 | const profile = useContext(ProfileContext);
25 | function clear() {
26 | setError("");
27 | }
28 |
29 | function createTournament() {
30 | if (!nameRef.current.value.trim()) {
31 | setError("Name can't be empty");
32 | return;
33 | }
34 |
35 | fetch("/api/tournament/new", {
36 | method: "POST",
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | body: JSON.stringify({
41 | name: nameRef.current.value,
42 | size: nbPlayers,
43 | }),
44 | })
45 | .then((res) => {
46 | if (!res.ok) {
47 | return res.json().then((errData) => {
48 | throw new Error(errData.error);
49 | });
50 | }
51 | location.route("/tournament/room");
52 | })
53 | .catch((err) => {
54 | setError(err.message);
55 | });
56 | }
57 |
58 | useEffect(() => {
59 | fetch(`/api/tournament/get_all_tournaments`).then((res) =>
60 | res.json().then((data) => {
61 | setTournaments(data.data);
62 | }),
63 | );
64 | }, []);
65 |
66 | return (
67 | <>
68 |
74 | Create a tournament
75 | {profile ? (
76 |
82 | ) : null}
83 | Numbers of players
84 |
85 | setNbPlayers(2)}
87 | className={`mx-1 aspect-square px-2 rounded-full border-2 border-white font-semibold ${nbPlayers == 2 ? "bg-white text-black" : ""}`}
88 | >
89 | 2
90 |
91 | setNbPlayers(4)}
93 | className={`mx-1 aspect-square px-2 rounded-full border-2 border-white font-semibold ${nbPlayers == 4 ? "bg-white text-black" : ""}`}
94 | >
95 | 4
96 |
97 | setNbPlayers(8)}
99 | className={`mx-1 aspect-square px-2 rounded-full border-2 border-white font-semibold ${nbPlayers == 8 ? "bg-white text-black" : ""}`}
100 | >
101 | 8
102 |
103 |
104 |
105 | Create
106 |
107 | {error}
108 |
109 |
110 |
111 |
112 |
113 | {language.create_room[lang]}
114 |
115 | setPopUp(true)}
117 | className="w-72 hover:w-96 h-96 px-2"
118 | >
119 |
120 |
121 | {language.create_room[lang]}
122 |
123 |
124 |
125 |
126 |
127 | {language.join_room[lang]}
128 |
129 |
{errorJoin}
130 |
131 | {tournaments.length ? (
132 | tournaments.map((room) => (
133 |
134 | ))
135 | ) : (
136 |
137 |
138 | {language.no_tournament[lang]}
139 |
140 |
141 | )}
142 |
143 |
144 |
145 | >
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/frontend/src/pages/Profile/index.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from 'react';
2 | import { ProfileCard } from '../../components/Profile/ProfileCard';
3 | import { Stat } from '../../components/Profile/Stat';
4 | import { FriendList } from '../../components/Profile/FriendList';
5 | import { GameHistory } from '../../components/Profile/GameHistory';
6 | import { language } from '../../scripts/languages'
7 | import { ProfileContext, LangContext } from '../../Contexts';
8 |
9 |
10 | export function Profile({fetchProfile, setProfile, setTriedLog}) {
11 | const profile = useContext(ProfileContext);
12 | const lang = useContext(LangContext);
13 |
14 | const [stats, setStats] = useState([
15 | {
16 | title: "Win ratio - Pong",
17 | labels: ["Win", "Lose"],
18 | colors: ["#32CD32", "#FF6961"],
19 | data: [
20 | profile
21 | ? profile.gameHistory.length
22 | ? profile.gameHistory.reduce(
23 | (accumulator, game) =>
24 | game.win && game.game == "Pong"
25 | ? accumulator + 1
26 | : accumulator,
27 | 0,
28 | )
29 | : 1
30 | : 1,
31 | profile
32 | ? profile.gameHistory.length
33 | ? profile.gameHistory.reduce(
34 | (accumulator, game) =>
35 | !game.win && game.game == "Pong"
36 | ? accumulator + 1
37 | : accumulator,
38 | 0,
39 | )
40 | : 1
41 | : 1,
42 | ],
43 | },
44 | {
45 | title: "Score ratio - Pong",
46 | colors: ["#79D2E6", "#FF964F"],
47 | labels: ["Scored", "Ceded"],
48 | data: [
49 | profile
50 | ? profile.gameHistory.length
51 | ? profile.gameHistory.reduce(
52 | (accumulator, game) =>
53 | game.game == "Pong"
54 | ? accumulator + game.score[0]
55 | : accumulator,
56 | 0,
57 | )
58 | : 1
59 | : 1,
60 | profile
61 | ? profile.gameHistory.length
62 | ? profile.gameHistory.reduce(
63 | (accumulator, game) =>
64 | game.game == "Pong"
65 | ? accumulator + game.score[1]
66 | : accumulator,
67 | 0,
68 | )
69 | : 1
70 | : 1,
71 | ],
72 | },
73 | {
74 | title: "Win ratio - Bonk",
75 | colors: ["#32CD32", "#FF6961"],
76 | labels: ["Win", "Lose"],
77 | data: [
78 | profile
79 | ? profile.gameHistory.length
80 | ? profile.gameHistory.reduce(
81 | (accumulator, game) =>
82 | game.win && game.game == "Bonk"
83 | ? accumulator + 1
84 | : accumulator,
85 | 0,
86 | )
87 | : 1
88 | : 1,
89 | profile
90 | ? profile.gameHistory.length
91 | ? profile.gameHistory.reduce(
92 | (accumulator, game) =>
93 | !game.win && game.game == "Bonk"
94 | ? accumulator + 1
95 | : accumulator,
96 | 0,
97 | )
98 | : 1
99 | : 1,
100 | ],
101 | },
102 | {
103 | title: "Score ratio - Bonk",
104 | colors: ["#79D2E6", "#FF964F"],
105 | labels: ["Kills", "Deaths"],
106 | data: [
107 | profile
108 | ? profile.gameHistory.length
109 | ? profile.gameHistory.reduce(
110 | (accumulator, game) =>
111 | game.game == "Bonk"
112 | ? accumulator + game.score[0]
113 | : accumulator,
114 | 0,
115 | )
116 | : 1
117 | : 1,
118 | profile
119 | ? profile.gameHistory.length
120 | ? profile.gameHistory.reduce(
121 | (accumulator, game) =>
122 | game.game == "Bonk"
123 | ? accumulator + game.score[1]
124 | : accumulator,
125 | 0,
126 | )
127 | : 1
128 | : 1,
129 | ],
130 | },
131 | ]);
132 |
133 | return (
134 |
135 |
136 |
137 |
{language.profile[lang]}
138 |
139 |
140 |
141 | {
142 | profile ?
143 | <>
144 |
145 |
146 | >
147 | : null
148 | }
149 |
150 |
151 |
152 |
153 | {
154 | stats.map((stat, index) => (
155 |
156 |
{stat.title}
157 |
163 |
164 | ))}
165 |
166 |
167 | );
168 | }
169 |
--------------------------------------------------------------------------------
/backend/ponk/auth.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.shortcuts import redirect
3 | from django.http.response import (
4 | HttpResponse,
5 | HttpResponseBadRequest,
6 | HttpResponseServerError,
7 | JsonResponse,
8 | )
9 | from django.contrib.auth.backends import BaseBackend
10 | from django.utils.regex_helper import _lazy_re_compile
11 | from django.contrib.auth import (
12 | authenticate,
13 | login,
14 | logout,
15 | )
16 | from django.contrib.auth.password_validation import validate_password
17 | from django.core.validators import RegexValidator
18 | from django.core.exceptions import ValidationError
19 | from ponk.ftapi import oauth_token, user_info, APIException
20 | from ponk.models import (
21 | User,
22 | AuthMethod,
23 | )
24 | import json
25 | import sys
26 |
27 |
28 | def validate_username(username):
29 | alpha_symbol = _lazy_re_compile(r"^[a-zA-Z0-9-+@!#]+\Z")
30 | validate_alpha_symbol = RegexValidator(
31 | alpha_symbol,
32 | "Username can only contain letters, numbers or -+@!#",
33 | "invalid",
34 | )
35 | validate_alpha_symbol(username)
36 | if username == "Lobby":
37 | raise ValidationError(
38 | "Lobby is a invalid username",
39 | params={"value": username},
40 | )
41 |
42 |
43 | class FtAuthBackend(BaseBackend):
44 | def authenticate(self, request, code):
45 | try:
46 | token = oauth_token(code)
47 | info = user_info(token)
48 | base_username = info.login
49 |
50 | try:
51 | num = 0
52 | while True:
53 | num += 1
54 | user = User.objects.get(username=info.login)
55 | print(f"GET: {user}")
56 | if user.auth_method == AuthMethod.FT:
57 | break
58 | info.login = f"{base_username}{num}"
59 | except User.DoesNotExist:
60 | user = User.objects.create_user(
61 | username=info.login, avatar=info.avatar, auth_method=AuthMethod.FT
62 | )
63 | print(f"CREATE: {user}")
64 | return user
65 | except APIException:
66 | return None
67 |
68 | def get_user(self, user_id):
69 | try:
70 | return User.objects.get(pk=user_id)
71 | except User.DoesNotExist:
72 | return None
73 |
74 |
75 | def oauth_login(request, *args, **kwargs):
76 | code = request.GET.get("code")
77 | if not code:
78 | return HttpResponseBadRequest()
79 |
80 | try:
81 | user = authenticate(request, code=code)
82 | if not user:
83 | return HttpResponseServerError()
84 | login(request, user, backend="ponk.auth.FtAuthBackend")
85 | return redirect("/play")
86 | except APIException:
87 | return HttpResponseServerError()
88 |
89 |
90 | def basic_login(request, *args, **kwargs):
91 | try:
92 | data = json.loads(request.body)
93 | user = authenticate(username=data["username"], password=data["password"])
94 |
95 | if not user:
96 | return JsonResponse(
97 | {
98 | "error": "Invalid username or password !",
99 | },
100 | status=403,
101 | )
102 | login(request, user, backend="django.contrib.auth.backends.ModelBackend")
103 | return JsonResponse(
104 | {
105 | "success": True,
106 | },
107 | status=200,
108 | )
109 | except BaseException as e:
110 | print(e, file=sys.stderr)
111 | return JsonResponse(
112 | {
113 | "error": "Fatal error !",
114 | },
115 | status=400,
116 | )
117 |
118 |
119 | def basic_register(request, *args, **kwargs):
120 | try:
121 | data = json.loads(request.body)
122 |
123 | try:
124 | user = User.objects.get(username=data["username"])
125 | except User.DoesNotExist:
126 | validate_username(data["username"])
127 | validate_password(data["password"])
128 | user = User.objects.create_user(
129 | username=data["username"],
130 | password=data["password"],
131 | auth_method=AuthMethod.BASIC,
132 | )
133 | login(request, user, backend="django.contrib.auth.backends.ModelBackend")
134 | return JsonResponse(
135 | {
136 | "success": True,
137 | },
138 | status=200,
139 | )
140 | return JsonResponse(
141 | {
142 | "error": "User already exist !",
143 | },
144 | status=409,
145 | )
146 | except ValidationError as e:
147 | return JsonResponse(
148 | {
149 | "error": e.messages[0],
150 | },
151 | status=403,
152 | )
153 | except BaseException as e:
154 | print(e, file=sys.stderr)
155 | return JsonResponse(
156 | {
157 | "error": "Fatal error !",
158 | },
159 | status=400,
160 | )
161 |
162 |
163 | def basic_logout(request, *args, **kwargs):
164 | try:
165 | logout(request)
166 | return JsonResponse(
167 | {
168 | "success": True,
169 | },
170 | status=200,
171 | )
172 | except BaseException as e:
173 | print(e, file=sys.stderr)
174 | return JsonResponse(
175 | {
176 | "error": "Fatal error !",
177 | },
178 | status=400,
179 | )
180 |
181 |
182 | urls = [
183 | path("42", oauth_login),
184 | path("login", basic_login),
185 | path("register", basic_register),
186 | path("logout", basic_logout),
187 | ]
188 |
--------------------------------------------------------------------------------
/backend/ponk/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for ponk project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.2.11.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.2/ref/settings/
11 | """
12 |
13 | from pathlib import Path
14 | import sys
15 | import os
16 |
17 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
18 | BASE_DIR = Path(__file__).resolve().parent.parent
19 |
20 | ASSETS_URL = "assets/"
21 | ASSETS_ROOT = "assets"
22 |
23 | # Quick-start development settings - unsuitable for production
24 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
25 |
26 | # SECURITY WARNING: keep the secret key used in production secret!
27 | SECRET_KEY = "django-insecure-cse^swp*#t9%=zipi6jh-h$don&c6^p6a7opmqgnk28f_fy*d7"
28 |
29 | # SECURITY WARNING: don't run with debug turned on in production!
30 | DEBUG = os.environ.get("FT_DEBUG", "n") != "n"
31 | DJANGO_VITE_DEV_MODE = DEBUG
32 |
33 | HOST = "localhost:8000"
34 | if not DEBUG:
35 | HOST = "localhost:8443"
36 | ALLOWED_HOSTS = [
37 | "localhost",
38 | "transcendence", # docker
39 | os.environ.get("HOST_IP_ADDRESS", "localhost"), # to test production
40 | HOST,
41 | ]
42 | else:
43 | ALLOWED_HOSTS = ["*"]
44 |
45 | # Application definition
46 |
47 | INSTALLED_APPS = [
48 | "daphne",
49 | "django_vite",
50 | "django.contrib.admin",
51 | "django.contrib.auth",
52 | "django.contrib.contenttypes",
53 | "django.contrib.sessions",
54 | "django.contrib.messages",
55 | "django.contrib.staticfiles",
56 | "ponk",
57 | ]
58 |
59 | MIDDLEWARE = [
60 | "django.contrib.sessions.middleware.SessionMiddleware",
61 | "django.middleware.common.CommonMiddleware",
62 | "django.contrib.auth.middleware.AuthenticationMiddleware",
63 | "django.contrib.messages.middleware.MessageMiddleware",
64 | ]
65 |
66 | ROOT_URLCONF = "ponk.urls"
67 |
68 | TEMPLATES = [
69 | {
70 | "BACKEND": "django.template.backends.django.DjangoTemplates",
71 | "DIRS": [BASE_DIR / "../frontend/"],
72 | "APP_DIRS": True,
73 | "OPTIONS": {
74 | "context_processors": [
75 | "django.template.context_processors.debug",
76 | "django.template.context_processors.request",
77 | "django.contrib.auth.context_processors.auth",
78 | "django.contrib.messages.context_processors.messages",
79 | ],
80 | },
81 | },
82 | ]
83 |
84 | WSGI_APPLICATION = "ponk.wsgi.application"
85 | ASGI_APPLICATION = "ponk.asgi.application"
86 | CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
87 |
88 |
89 | # Database
90 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
91 |
92 | if not "DB_NAME" in os.environ:
93 | print('No "DB_NAME" in environ file !', file=sys.stderr)
94 | sys.exit(1)
95 | if not "DB_USER" in os.environ:
96 | print('No "DB_USER" in environ file !', file=sys.stderr)
97 | sys.exit(1)
98 | if not "DB_PASS" in os.environ:
99 | print('No "DB_PASS" in environ file !', file=sys.stderr)
100 | sys.exit(1)
101 |
102 | DB_NAME = os.environ["DB_NAME"]
103 | DB_USER = os.environ["DB_USER"]
104 | DB_PASS = os.environ["DB_PASS"]
105 |
106 | DATABASES = {
107 | "default": {
108 | "ENGINE": "django.db.backends.postgresql",
109 | "NAME": DB_NAME,
110 | "USER": DB_USER,
111 | "PASSWORD": DB_PASS,
112 | "HOST": "127.0.0.1",
113 | "PORT": "5432",
114 | }
115 | }
116 |
117 | AUTH_USER_MODEL = "ponk.User"
118 |
119 | # Password validation
120 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
121 |
122 | AUTH_PASSWORD_VALIDATORS = [
123 | {
124 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
125 | },
126 | {
127 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
128 | },
129 | {
130 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
131 | },
132 | {
133 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
134 | },
135 | ]
136 |
137 | AUTHENTICATION_BACKENDS = [
138 | "ponk.auth.FtAuthBackend",
139 | "django.contrib.auth.backends.ModelBackend",
140 | ]
141 |
142 | AUTH_PASSWORD_VALIDATORS = [
143 | {
144 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
145 | },
146 | {
147 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
148 | "OPTIONS": {
149 | "min_length": 6,
150 | },
151 | },
152 | {
153 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
154 | },
155 | ]
156 |
157 | # Internationalization
158 | # https://docs.djangoproject.com/en/4.2/topics/i18n/
159 |
160 | LANGUAGE_CODE = "en-us"
161 |
162 | TIME_ZONE = "UTC"
163 |
164 | USE_I18N = True
165 |
166 | USE_TZ = True
167 |
168 |
169 | # Static files (CSS, JavaScript, Images)
170 | # https://docs.djangoproject.com/en/4.2/howto/static-files/
171 |
172 | FRONTEND_DIR = BASE_DIR.parent / "frontend"
173 |
174 | STATIC_ROOT = BASE_DIR.parent / ".static"
175 | STATIC_URL = "/static/"
176 |
177 | DJANGO_VITE_ASSETS_PATH = FRONTEND_DIR / "build"
178 | DJANGO_VITE_MANIFEST_PATH = DJANGO_VITE_ASSETS_PATH / "manifest.json"
179 |
180 | STATICFILES_DIRS = [
181 | FRONTEND_DIR / "pong-client/pkg",
182 | FRONTEND_DIR / "bonk-client",
183 | DJANGO_VITE_ASSETS_PATH,
184 | ]
185 |
186 | # Default primary key field type
187 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
188 |
189 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
190 |
191 |
192 | # 42 API
193 | API_URL = "https://api.intra.42.fr"
194 | CLIENT_ID = os.environ.get("CLIENT_ID")
195 | CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
196 | REDIRECT_URI = f"http://{HOST}/auth/42"
197 | if not DEBUG:
198 | REDIRECT_URI = f"https://{HOST}/auth/42"
199 |
--------------------------------------------------------------------------------