├── 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="Bonk Corporation %% " 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 | 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 |
12 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/Shop/BallPresentation.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function BallPresentation(props) { 4 | return ( 5 |
6 |
9 |
12 |
15 |
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 |
22 | 28 | 36 | 41 | 42 |
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 | ![Screenshot from 2024-05-31 20-08-24](https://github.com/Bonk-Corporation/ft_transcendence/assets/114430228/a35969cd-3bf0-4385-aa9a-3da83de064f2) 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 |
21 |
22 | 23 |
24 |

25 | '{props.item.name}' skin pack 26 |

27 |
28 | 29 |

30 | {props.item.price * 2}€ 31 |

32 |
33 |
34 |
35 | 40 | {props.possessed ? ( 41 | 42 | {props.selected ? language.deselect[lang] : language.select[lang]} 43 | 44 | ) : ( 45 | 50 | )} 51 | 52 |
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 | 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 |
62 | 63 | 64 | 65 |

{error}

66 |
67 | {props.setTriedLog(true); handleSignup()}} className="my-2">{language.sign_up[lang]} 68 |
69 |
70 |
71 |

{language.already_account[lang]}

72 | {language.log_in[lang]} 73 |
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 | 54 | 60 |
61 | ) : ( 62 | 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 | 30 |

PONG

31 |

{language.play_against[lang]}

32 |
33 |
setMode("bot")} 35 | className={`${mode == "bot" ? "bg-white text-black font-semibold" : ""} mr-2 rounded px-4 py-2 border-2 border-white transition-all cursor-pointer`} 36 | > 37 | {language.bot[lang]} 38 |
39 |
setMode("player")} 41 | className={`${mode == "player" ? "bg-white text-black font-semibold" : ""} ml-2 rounded px-4 py-2 border-2 border-white transition-all cursor-pointer`} 42 | > 43 | {language.player[lang]} 44 |
45 |
46 | 47 | Play! 48 | 49 |
50 |

54 | Right Side 55 |

56 | 60 |

61 | BOLVIC WIN 62 |

63 |
64 |

65 | 4 - 1 66 |

67 |
68 | 69 | Play again 70 | 71 | 72 | Home 73 | 74 |
75 |
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 | 66 |

BONK

67 | {language.play[lang]} 68 |
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 |
61 |

{language.log_in[lang]}

62 |
68 | 74 | 80 |

{error}

81 |
82 | 83 | {language.log_in[lang]} 84 | 85 | setTriedLog(true)} className="underline" href={url}> 86 | {language.log_in_42[lang]} 87 | 88 |
89 |
90 |
91 |

{language.no_account[lang]}

92 | 93 | {language.sign_up[lang]} 94 | 95 |
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 | 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 |
135 |
136 | {messages[activeRoom]?.map(([where, message]) => ( 137 | {message} 138 | ))} 139 |
140 |
141 |
142 | 146 |
147 |
148 | 149 | ) : ( 150 |
151 |

Chat

152 |
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 | 91 | 97 | 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 | --------------------------------------------------------------------------------