Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b40dad8f57 | |||
| 23cca7a249 | |||
| e1e529b3ca | |||
| e96c16819d | |||
| aefb858247 | |||
| a9f81b4d65 | |||
| 4b3909c1a3 | |||
| 4fa835b62a | |||
| b31436a40a | |||
| f9587c5cfa | |||
| 44a0ffe743 | |||
| 0f0e777e6e | |||
| c96629b704 | |||
| 41612f5d39 | |||
| e1573ba9f0 | |||
| b9c4c817f8 | |||
| 384363c8dd | |||
| def9918047 | |||
| cafa0cccc4 | |||
| 8b907d5a86 |
@@ -1,6 +1,6 @@
|
||||
*This project has been created as part of the 42 curriculum by agallon, gprunet, yantoine and tfauve-p*
|
||||
|
||||
***DESCRIPTION***
|
||||
**DESCRIPTION**
|
||||
|
||||
For starters, ft_transcendence is a wonderful project based on building a web-application running from Docker containers where the goal is, for the first time to do whatever we want, yet still we need to follow multiples criteria based on a number of points to grind to set the project as finished.
|
||||
|
||||
@@ -8,22 +8,22 @@ For such a project our group thought about the "CATETRIBBL.IO". We chose to make
|
||||
|
||||
But beware ! A mysterious noble cat named Wiskas The Third is gone ... It is said that he's been lurking around trapping 42's students into infinite conversation known as "tunnel". If you see him please report to us as soon as possible !
|
||||
|
||||
***INSTRUCTIONS***
|
||||
**INSTRUCTIONS**
|
||||
|
||||
Like every 42 project you will need to git clone it into a valid repository, then add our functional .env file at the root of the repository. After all that, make use of the "make" command and watch our fabulous containers building themselves ! Look for https://localhost:8443/ once it's built, remember that you need to login in order to play on our web app !
|
||||
|
||||
Outside of 42 environment you will obviously need Docker and Make installed.
|
||||
|
||||
***RESOURCES***
|
||||
**RESOURCES**
|
||||
|
||||
https://www.geeksforgeeks.org
|
||||
https://developer.mozilla.org/fr/docs/Web/JavaScript
|
||||
https://www.w3schools.com/js/
|
||||
https://www.tigerdata.com/learn/postgres-cheat-sheet
|
||||
https://www.programiz.com/css/button-styling
|
||||
https://developer.mozilla.org/fr/docs/Web/CSS
|
||||
https://chatgpt.com/
|
||||
https://www.gimp.org/tutorials/
|
||||
- https://www.geeksforgeeks.org
|
||||
- https://developer.mozilla.org/fr/docs/Web/JavaScript
|
||||
- https://www.w3schools.com/js/
|
||||
- https://www.tigerdata.com/learn/postgres-cheat-sheet
|
||||
- https://www.programiz.com/css/button-styling
|
||||
- https://developer.mozilla.org/fr/docs/Web/CSS
|
||||
- https://chatgpt.com/
|
||||
- https://www.gimp.org/tutorials/
|
||||
|
||||
AI was mostly used to ask questions and deepen understanding, it was also used to generate multiple samples of what we could do front-end wise.
|
||||
|
||||
@@ -40,20 +40,20 @@ Use of the framework Express for the back-end because its compatible with jsonwe
|
||||
|
||||
**DEPENDENCIES**
|
||||
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"socket.io": "^4.6.1",
|
||||
"cors": "^2.8.5",
|
||||
"passport": "0.7.0",
|
||||
"passport-github2": "0.1.12",
|
||||
"express-session": "1.18.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"file-type": "^19.0.0"
|
||||
- "express": "^4.18.2",
|
||||
- "pg": "^8.11.3",
|
||||
- "bcrypt": "^5.1.0",
|
||||
- "jsonwebtoken": "^9.0.2",
|
||||
- "dotenv": "^17.2.3",
|
||||
- "socket.io": "^4.6.1",
|
||||
- "cors": "^2.8.5",
|
||||
- "passport": "0.7.0",
|
||||
- "passport-github2": "0.1.12",
|
||||
- "express-session": "1.18.0",
|
||||
- "multer": "^1.4.5-lts.1",
|
||||
- "file-type": "^19.0.0"
|
||||
|
||||
***TEAM INFORMATION***
|
||||
**TEAM INFORMATION**
|
||||
|
||||
Tfauve-p : The project manager, is in lead of organizing all the meeting with the team which changed over time, including then some recruitment. There was some adjustments to make over our vision of the project while coding it on GitHub.
|
||||
|
||||
@@ -63,7 +63,7 @@ Gprunet : The technical lead, is in charge of the back-end, made some strong dec
|
||||
|
||||
Agallon : Developer in the front-end action, he joined the team after the project was done but managed to innovate and brought to life the marvelous Wiskas the Third. Furthermore he also cared about the integrity of the web application and greatly improved the user experience through logical decisions.
|
||||
|
||||
***Project Management***
|
||||
**PROJECT MANAGEMENT**
|
||||
|
||||
The task's sharing was based on our own advance of the 42 cursus, meaning that Gprunet and Yantoine started coding the app sooner than Tfauve-p and Agallon.
|
||||
|
||||
@@ -73,7 +73,7 @@ Tfauve-p: Front-end Designer
|
||||
Agallon: Adjustements on Front-end and some new features.
|
||||
|
||||
|
||||
***TECHNICAL STACK***
|
||||
**TECHNICAL STACK**
|
||||
|
||||
Front-end: JavaScript, HTML, CSS, NGINX
|
||||
Back-end: JavaScript, Express, JWT, multer, etc...
|
||||
@@ -83,19 +83,25 @@ Since python doesn't have many front-end framework we opted to use JavaScript fo
|
||||
After learning about JWT and learning that Express had a great synergy with it the choice was natural.
|
||||
|
||||
|
||||
***DATABASES SCHEMA***
|
||||
**DATABASES SCHEMA**
|
||||
|
||||

|
||||
|
||||
|
||||
**FEATURES LIST**
|
||||
|
||||
***FEATURES LIST***
|
||||
- 2 Games
|
||||
- One talking Cat
|
||||
- Friends chat
|
||||
-
|
||||
|
||||
***MODULES**
|
||||
**MODULES**
|
||||
|
||||
Total : 24pts ( 14pts for 100% 19pts for 125% )
|
||||
Total : 23pts ( 14pts for 100% 19pts for 125% )
|
||||
|
||||
**WEB**
|
||||
- WEB
|
||||
|
||||
Major : Use a back end and front end framework
|
||||
Minor : Use a back end framework
|
||||
|
||||
Major : Implement real-time features
|
||||
|
||||
@@ -105,11 +111,11 @@ Major : A public API to interact with the database
|
||||
|
||||
Minor : A complete notification system for all creation, update and deletion account
|
||||
|
||||
**ACCESSIBILITY**
|
||||
- ACCESSIBILITY
|
||||
|
||||
Minor : Support for additional browsers
|
||||
|
||||
**USER MANAGEMENT**
|
||||
- USER MANAGEMENT
|
||||
|
||||
Major : Standard user management and authentication
|
||||
|
||||
@@ -117,7 +123,7 @@ Minor : Game statistics and match history ???
|
||||
|
||||
Minor : Implement remote authentication
|
||||
|
||||
**GAMING AND USER EXPERIENCE**
|
||||
- GAMING AND USER EXPERIENCE
|
||||
|
||||
Major : Implement a complete web-based game where users can play against each other
|
||||
|
||||
@@ -133,6 +139,4 @@ Minor : Game customization options
|
||||
|
||||
Minor : Spectator mode for games
|
||||
|
||||
**INDIVIDUAL CONTRIBUTION**
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
all : up
|
||||
|
||||
up :
|
||||
@docker compose -f ./docker-compose.yml build --no-cache
|
||||
@docker compose -f ./docker-compose.yml up -d
|
||||
|
||||
clean :
|
||||
@@ -12,3 +13,446 @@ fclean :
|
||||
|
||||
re : fclean up
|
||||
|
||||
|
||||
|
||||
# ╭────────────────────────────────────────────────────────────────────────────╮
|
||||
# │─██████████████─██████████████─██████████████─██████─────────██████████████─│
|
||||
# │─██░░░░░░░░░░██─██░░░░░░░░░░██─██░░░░░░░░░░██─██░░██─────────██░░░░░░░░░░██─│
|
||||
# │─██████░░██████─██░░██████░░██─██░░██████░░██─██░░██─────────██░░██████████─│
|
||||
# │─────██░░██─────██░░██──██░░██─██░░██──██░░██─██░░██─────────██░░██─────────│
|
||||
# │─────██░░██─────██░░██──██░░██─██░░██──██░░██─██░░██─────────██░░██████████─│
|
||||
# │─────██░░██─────██░░██──██░░██─██░░██──██░░██─██░░██─────────██░░░░░░░░░░██─│
|
||||
# │─────██░░██─────██░░██──██░░██─██░░██──██░░██─██░░██─────────██████████░░██─│
|
||||
# │─────██░░██─────██░░██──██░░██─██░░██──██░░██─██░░██─────────────────██░░██─│
|
||||
# │─────██░░██─────██░░██████░░██─██░░██████░░██─██░░██████████─██████████░░██─│
|
||||
# │─────██░░██─────██░░░░░░░░░░██─██░░░░░░░░░░██─██░░░░░░░░░░██─██░░░░░░░░░░██─│
|
||||
# │─────██████─────██████████████─██████████████─██████████████─██████████████─│
|
||||
# ╰────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
# --------------------------------------------------------------------------------- >
|
||||
VALGRIND = valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s --track-fds=yes --trace-children=yes $(V_FLAG)
|
||||
|
||||
# ↑さ↓ぎょう を ↓ほ↑ぞん
|
||||
# Default git push
|
||||
git: fclean
|
||||
@$(call random_shmol_cat_blink, 作業を保存してるかな.., いいね、いいねえー , $(CLS), );
|
||||
@current_date=$$(date); \
|
||||
git add .; \
|
||||
git commit -m "^^._, work in progress, small changes"; \
|
||||
git push
|
||||
|
||||
# Git Push that asks for commit msg
|
||||
git2: fclean
|
||||
@$(call random_shmol_cat_blink, 作業を保存してるかな.., いいね、いいねえー , $(CLS), );
|
||||
@read -p "Enter commit message: " msg; \
|
||||
[ -z "$$msg" ] && msg=$$(date); \
|
||||
git add .; \
|
||||
git commit -m "$$msg"; \
|
||||
git push
|
||||
|
||||
# Git Push use the content of .gitmsg to push
|
||||
# if .gitmsg empty, return error
|
||||
# clear .gitmsg on succesfull push.
|
||||
GIT_MSG_FILE = ../.gitmsg
|
||||
git3: fclean
|
||||
@$(call random_shmol_cat_blink, 作業を保存してるかな.., いいね、いいねえー , $(CLS), );
|
||||
@{ \
|
||||
msg="$$(cat $(GIT_MSG_FILE) 2>/dev/null)"; \
|
||||
[ -z "$$msg" ] && { $(call random_shmol_cat_blink, error, file is empty, , ); exit 1; }; \
|
||||
git add . && \
|
||||
git commit -m "$$msg" && \
|
||||
git push && \
|
||||
: > $(GIT_MSG_FILE) && \
|
||||
$(call random_shmol_cat_blink, success!, $(GIT_MSG_FILE) cleared., , ); \
|
||||
}
|
||||
|
||||
|
||||
.SILENT: $(NAME)
|
||||
|
||||
|
||||
# ╭────────────────────────────────────────────────────────────────────────────────────╮
|
||||
# │─██████████████─████████████████───██████████─██████──────────██████─██████████████─│
|
||||
# │─██░░░░░░░░░░██─██░░░░░░░░░░░░██───██░░░░░░██─██░░██████████──██░░██─██░░░░░░░░░░██─│
|
||||
# │─██░░██████░░██─██░░████████░░██───████░░████─██░░░░░░░░░░██──██░░██─██████░░██████─│
|
||||
# │─██░░██──██░░██─██░░██────██░░██─────██░░██───██░░██████░░██──██░░██─────██░░██─────│
|
||||
# │─██░░██████░░██─██░░████████░░██─────██░░██───██░░██──██░░██──██░░██─────██░░██─────│
|
||||
# │─██░░░░░░░░░░██─██░░░░░░░░░░░░██─────██░░██───██░░██──██░░██──██░░██─────██░░██─────│
|
||||
# │─██░░██████████─██░░██████░░████─────██░░██───██░░██──██░░██──██░░██─────██░░██─────│
|
||||
# │─██░░██─────────██░░██──██░░██───────██░░██───██░░██──██░░██████░░██─────██░░██─────│
|
||||
# │─██░░██─────────██░░██──██░░██████─████░░████─██░░██──██░░░░░░░░░░██─────██░░██─────│
|
||||
# │─██░░██─────────██░░██──██░░░░░░██─██░░░░░░██─██░░██──██████████░░██─────██░░██─────│
|
||||
# │─██████─────────██████──██████████─██████████─██████──────────██████─────██████─────│
|
||||
# ╰────────────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
# C_213
|
||||
PURPLE = \033[38;5;97m
|
||||
# C_430
|
||||
GOLD = \033[38;5;178m
|
||||
# C_040
|
||||
GREEN1 = \033[38;5;40m
|
||||
# C_045
|
||||
BLUE1 = \033[38;5;45m
|
||||
|
||||
# $(C_105), $(C_510), $(C_025)
|
||||
# $(RED), $(GOLD), $(BLUE1)
|
||||
|
||||
test_color666:
|
||||
@$(call random_cat, $(call pad_word, 12, The⠀Cake), $(call pad_word, 14, Is⠀A⠀Lie⠀...), $(CLS), $(RESET));
|
||||
@$(call random_cat, $(call pad_word, 13, The⠀Cake), $(call pad_word, 15, Is⠀A⠀Lie⠀...), , $(RESET));
|
||||
|
||||
|
||||
# $(call pad_word, 12, The⠀Cake)
|
||||
pad_word = $(BLINK)$(shell printf "%$(1)s" "$(2)")$(RESET)
|
||||
# improve with: STRING1=$$(printf "\033[38;5;%dm" $$(shuf -i 0-255 -n 1));
|
||||
|
||||
# --------------------------------------------------------------------------------- >
|
||||
# @$(call print_cat, $(CLEAR), $(body), $(eye), $(txt), $(call pad_word, 12, "The⠀Cake"), $(call pad_word, 12, "Is⠀A⠀Lie..."));
|
||||
# print_cat (resest?)(C_c)_sCtt$padded_txt_top))($(padded_txt_bot))
|
||||
define print_cat
|
||||
echo -e "$(1)$(2)\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠘⡄⠀⠀⠀⠀⠀⠀⣀⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠁⠉⠉⠉⠒⠊⠉⠀⡇⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜$(3)$(BLINK)⣀⡀$(RESET)$(2)⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠲⢴⠁$(3)$(BLINK)⠛⠁$(RESET)$(2)$(3)$(BLINK)⢀⣄$(RESET)$(2)⠀⠀⠀⢸⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠺⡀⠀⠀⢶⠤$(3)$(BLINK)⠈⠋$(RESET)$(2)⠀⠀⠀⡘⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⡀⠀⠀⠀⠠⣉⠑⠂⠀⢠⠃⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠁⠀⠀⠈⢆⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡆⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠒⠃⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠀\n\
|
||||
\t\t\t\t\t ⠀⠔⠑⠄⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇\n\
|
||||
\t\t\t\t\t ⠸⡀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⡇$(4)$(5)$(2)⠀⠀⠀⠀⠀⡇\n\
|
||||
\t\t\t\t\t ⠀⠱⡀⠀⠳⡀⠀⠀⠀⠀⠀⠀⢃$(4)$(6)$(2)⠀⠀⡸⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠑⢄⠀⠈⠒⢄⡀⠀⠀⠀⠸⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠑⠦⣀⠀⠈⠉⠐⠒⠒⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⢢⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠉⠐⠒⠠⠤⠤⠤⠤⠔⠂⠠⠤⠤⠤⠄⠐⠒⠂⠉⠉⠉⠉⠉⠁\n$(RESET)"
|
||||
endef
|
||||
|
||||
# --------------------------------------------------------------------------------- >
|
||||
# @$(call random_cat, $(call pad_word, 12, txt1), $(call pad_word, 12, txt2), $(CLS), $(RESET));
|
||||
# print_cat (resest?)(C_c)_sCtt$padded_txt_top))($(padded_txt_bot))
|
||||
define random_cat
|
||||
COLOR=$$(printf "\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
COLOR2=$$(printf "\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
COLOR3=$$(printf "\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
echo -e "$(3)$${COLOR}\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠘⡄⠀⠀⠀⠀⠀⠀⣀⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠁⠉⠉⠉⠒⠊⠉⠀⡇⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀$${COLOR2}$(BLINK)⣀⡀$(RESET)$${COLOR}⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠲⢴⠁⠀$${COLOR2}$(BLINK)⠛⠁$(RESET)$${COLOR}$${COLOR2}$(BLINK)⠀⠀⢀⣄$(RESET)$${COLOR}⠀⠀⠀⠀⢸⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠺⡀⠀⠀⢶⠤$${COLOR2}$(BLINK)⠀⠈⠋$(RESET)$${COLOR}⠀⠀⠀⠀⡘⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⡀⠀⠀⠀⠠⣉⠑⠂⠀⢠⠃⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠁⠀⠀⠈⢆⠀⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡆⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠒⠃⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠀\n\
|
||||
\t\t\t\t\t ⠀⠔⠑⠄⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇\n\
|
||||
\t\t\t\t\t ⠸⡀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⡇$${COLOR3}$(1)$${COLOR}⠀⠀⠀⠀⠀⡇\n\
|
||||
\t\t\t\t\t ⠀⠱⡀⠀⠳⡀⠀⠀⠀⠀⠀⠀⢃$${COLOR3}$(2)$${COLOR}⠀⠀⡸⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠑⢄⠀⠈⠒⢄⡀⠀⠀⠀⠸⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠑⠦⣀⠀⠈⠉⠐⠒⠒⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⢢⠀\n\
|
||||
\t\t\t\t\t ⠀⠀⠀⠀⠀⠀⠀⠉⠐⠒⠠⠤⠤⠤⠤⠔⠂⠠⠤⠤⠤⠄⠐⠒⠂⠉⠉⠉⠉⠉⠁\n$(4)"
|
||||
endef
|
||||
|
||||
# --------------------------------------------------------------------------------- >
|
||||
# @$(call shmol_cat_color, $(C_c), $(C_t), txt1, txt2, $(CLS), $(RESET));
|
||||
define shmol_cat_color
|
||||
echo -e "$(5)$(2)\
|
||||
\tにゃ~$(1)\t⠀╱|、\n\
|
||||
\t\t(˚ˎ。7⠀⠀⠀$(2)~ $(3) ~$(1)\n\
|
||||
\t\t⠀|、˜\\\\\t\t$(2)$(4)$(1)\n\
|
||||
\t\t⠀じしˍ)ノ\n$(6)"
|
||||
endef
|
||||
# --------------------------------------------------------------------------------- >
|
||||
# @$(call random_shmol_cat, text 1, text 2, $(CLS), $(RESET));
|
||||
# $(1)= $(CLEAR); $(2)= text1; $(3)= text2; $(4)= $(RESET)
|
||||
define random_shmol_cat
|
||||
COLOR=$$(printf "\033[38;5;%dm" $$(shuf -i 1-255 -n 1)); \
|
||||
COLOR2=$$(printf "\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
echo -e "$(3)$${COLOR2}\
|
||||
\tにゃ~$${COLOR}\t⠀╱|、\n\
|
||||
\t\t(˚ˎ。7⠀⠀⠀$${COLOR2}~ $(1) ~$${COLOR}\n\
|
||||
\t\t⠀|、˜\\\\\t\t$${COLOR2}~ $(2)$${COLOR}\n\
|
||||
\t\t⠀じしˍ)ノ\n$(4)"
|
||||
endef
|
||||
|
||||
# // <!> - - - - - - - - - - - </!>
|
||||
# --------------------------------------------------------------------------------- >
|
||||
rscs:
|
||||
@$(call random_shmol_cat_surligne, text 1, text 2, $(CLS), $(RESET));
|
||||
|
||||
define random_shmol_cat_surligne
|
||||
COLOR=$$(printf "\033[0m\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
COLOR2=$$(printf "\033[48;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
echo -e "$(3)$${COLOR2}\
|
||||
\tにゃ~$${COLOR}\t⠀╱|、\n\
|
||||
\t\t(˚ˎ。7⠀⠀⠀$${COLOR2}~ $(1) ~$${COLOR}\n\
|
||||
\t\t⠀|、˜\\\\\t\t$${COLOR2}~ $(2)$${COLOR}\n\
|
||||
\t\t⠀じしˍ)ノ\n$(4)"
|
||||
endef
|
||||
|
||||
rscb:
|
||||
@$(call random_shmol_cat_blink, text 1, text 2, $(CLS), $(RESET));
|
||||
|
||||
define random_shmol_cat_blink
|
||||
COLOR=$$(printf "\033[0m\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
COLOR2=$$(printf "\e[5m\033[38;5;%dm" $$(shuf -i 0-255 -n 1)); \
|
||||
echo -e "$(3)\n$${COLOR2}\
|
||||
\tにゃ~$${COLOR}\t⠀╱|、\n\
|
||||
\t\t(˚ˎ。7⠀⠀⠀$${COLOR2}~ $(1) ~$${COLOR}\n\
|
||||
\t\t⠀|、˜\\\\\t\t$${COLOR2}~ $(2)$${COLOR}\n\
|
||||
\t\t⠀じしˍ)ノ\n$(4)"
|
||||
endef
|
||||
# // <!> - - - - - - - - - - - </!>
|
||||
# --------------------------------------------------------------------------------- >
|
||||
# @$(call shmol_cat_error, $(RED), $(RED_L));
|
||||
# $(1) = $(C_c)$2) = $(C_ttN CLS
|
||||
define shmol_cat_error
|
||||
echo -e "$(2)\
|
||||
\tにゃ~$(1)\t⠀╱|、\n\
|
||||
\t\t(˚ˎ。7⠀⠀⠀$(2)~ somshin wen wong ~$(1)\n\
|
||||
\t\t⠀|、˜\\\\\n\
|
||||
\t\t⠀じしˍ)ノ\n$(RESET)"
|
||||
endef
|
||||
|
||||
# Define all 256 colors
|
||||
CLEAR = \033[2J\033[H
|
||||
CLS = \033[2J\033[H
|
||||
RESET = \033[0m
|
||||
BLINK = \033[5m
|
||||
# U+2800 to U+28FF Braile
|
||||
# <Esc>[38;5;ColorNumberm
|
||||
BLACK = \033[38;5;0m
|
||||
RED = \033[38;5;1m
|
||||
GREEN = \033[38;5;2m
|
||||
YELLOW = \033[38;5;3m
|
||||
BLUE = \033[38;5;4m
|
||||
MAGENTA = \033[38;5;5m
|
||||
CYAN = \033[38;5;6m
|
||||
GRAY = \033[38;5;7m
|
||||
|
||||
BLACK_L = \033[38;5;8m
|
||||
RED_L = \033[38;5;9m
|
||||
GREEN_L = \033[38;5;10m
|
||||
YELLOW_L = \033[38;5;11m
|
||||
BLUE_L = \033[38;5;12m
|
||||
MAGENTA_L = \033[38;5;13m
|
||||
CYAN_L = \033[38;5;14m
|
||||
WHITE = \033[38;5;15m
|
||||
|
||||
C_000 = \033[38;5;16m
|
||||
C_001 = \033[38;5;17m
|
||||
C_002 = \033[38;5;18m
|
||||
C_003 = \033[38;5;19m
|
||||
C_004 = \033[38;5;20m
|
||||
C_005 = \033[38;5;21m
|
||||
C_010 = \033[38;5;22m
|
||||
C_011 = \033[38;5;23m
|
||||
C_012 = \033[38;5;24m
|
||||
C_013 = \033[38;5;25m
|
||||
C_014 = \033[38;5;26m
|
||||
C_015 = \033[38;5;27m
|
||||
C_020 = \033[38;5;28m
|
||||
C_021 = \033[38;5;29m
|
||||
C_022 = \033[38;5;30m
|
||||
C_023 = \033[38;5;31m
|
||||
C_024 = \033[38;5;32m
|
||||
C_025 = \033[38;5;33m
|
||||
C_030 = \033[38;5;34m
|
||||
C_031 = \033[38;5;35m
|
||||
C_032 = \033[38;5;36m
|
||||
C_033 = \033[38;5;37m
|
||||
C_034 = \033[38;5;38m
|
||||
C_035 = \033[38;5;39m
|
||||
C_040 = \033[38;5;40m
|
||||
C_041 = \033[38;5;41m
|
||||
C_042 = \033[38;5;42m
|
||||
C_043 = \033[38;5;43m
|
||||
C_044 = \033[38;5;44m
|
||||
C_045 = \033[38;5;45m
|
||||
C_050 = \033[38;5;46m
|
||||
C_051 = \033[38;5;47m
|
||||
C_052 = \033[38;5;48m
|
||||
C_053 = \033[38;5;49m
|
||||
C_054 = \033[38;5;50m
|
||||
C_055 = \033[38;5;51m
|
||||
C_100 = \033[38;5;52m
|
||||
C_101 = \033[38;5;53m
|
||||
C_102 = \033[38;5;54m
|
||||
C_103 = \033[38;5;55m
|
||||
C_104 = \033[38;5;56m
|
||||
C_105 = \033[38;5;57m
|
||||
C_110 = \033[38;5;58m
|
||||
C_111 = \033[38;5;59m
|
||||
C_112 = \033[38;5;60m
|
||||
C_113 = \033[38;5;61m
|
||||
C_114 = \033[38;5;62m
|
||||
C_115 = \033[38;5;63m
|
||||
C_120 = \033[38;5;64m
|
||||
C_121 = \033[38;5;65m
|
||||
C_122 = \033[38;5;66m
|
||||
C_123 = \033[38;5;67m
|
||||
C_124 = \033[38;5;68m
|
||||
C_125 = \033[38;5;69m
|
||||
C_130 = \033[38;5;70m
|
||||
C_131 = \033[38;5;71m
|
||||
C_132 = \033[38;5;72m
|
||||
C_133 = \033[38;5;73m
|
||||
C_134 = \033[38;5;74m
|
||||
C_135 = \033[38;5;75m
|
||||
C_140 = \033[38;5;76m
|
||||
C_141 = \033[38;5;77m
|
||||
C_142 = \033[38;5;78m
|
||||
C_143 = \033[38;5;79m
|
||||
C_144 = \033[38;5;80m
|
||||
C_145 = \033[38;5;81m
|
||||
C_150 = \033[38;5;82m
|
||||
C_151 = \033[38;5;83m
|
||||
C_152 = \033[38;5;84m
|
||||
C_153 = \033[38;5;85m
|
||||
C_154 = \033[38;5;86m
|
||||
C_155 = \033[38;5;87m
|
||||
C_200 = \033[38;5;88m
|
||||
C_201 = \033[38;5;89m
|
||||
C_202 = \033[38;5;90m
|
||||
C_203 = \033[38;5;91m
|
||||
C_204 = \033[38;5;92m
|
||||
C_205 = \033[38;5;93m
|
||||
C_210 = \033[38;5;94m
|
||||
C_211 = \033[38;5;95m
|
||||
C_212 = \033[38;5;96m
|
||||
C_213 = \033[38;5;97m
|
||||
C_214 = \033[38;5;98m
|
||||
C_215 = \033[38;5;99m
|
||||
C_220 = \033[38;5;100m
|
||||
C_221 = \033[38;5;101m
|
||||
C_222 = \033[38;5;102m
|
||||
C_223 = \033[38;5;103m
|
||||
C_224 = \033[38;5;104m
|
||||
C_225 = \033[38;5;105m
|
||||
C_230 = \033[38;5;106m
|
||||
C_231 = \033[38;5;107m
|
||||
C_232 = \033[38;5;108m
|
||||
C_233 = \033[38;5;109m
|
||||
C_234 = \033[38;5;110m
|
||||
C_235 = \033[38;5;111m
|
||||
C_240 = \033[38;5;112m
|
||||
C_241 = \033[38;5;113m
|
||||
C_242 = \033[38;5;114m
|
||||
C_243 = \033[38;5;115m
|
||||
C_244 = \033[38;5;116m
|
||||
C_245 = \033[38;5;117m
|
||||
C_250 = \033[38;5;118m
|
||||
C_251 = \033[38;5;119m
|
||||
C_252 = \033[38;5;120m
|
||||
C_253 = \033[38;5;121m
|
||||
C_254 = \033[38;5;122m
|
||||
C_255 = \033[38;5;123m
|
||||
C_300 = \033[38;5;124m
|
||||
C_301 = \033[38;5;125m
|
||||
C_302 = \033[38;5;126m
|
||||
C_303 = \033[38;5;127m
|
||||
C_304 = \033[38;5;128m
|
||||
C_305 = \033[38;5;129m
|
||||
C_310 = \033[38;5;130m
|
||||
C_311 = \033[38;5;131m
|
||||
C_312 = \033[38;5;132m
|
||||
C_313 = \033[38;5;133m
|
||||
C_314 = \033[38;5;134m
|
||||
C_315 = \033[38;5;135m
|
||||
C_320 = \033[38;5;136m
|
||||
C_321 = \033[38;5;137m
|
||||
C_322 = \033[38;5;138m
|
||||
C_323 = \033[38;5;139m
|
||||
C_324 = \033[38;5;140m
|
||||
C_325 = \033[38;5;141m
|
||||
C_330 = \033[38;5;142m
|
||||
C_331 = \033[38;5;143m
|
||||
C_332 = \033[38;5;144m
|
||||
C_333 = \033[38;5;145m
|
||||
C_334 = \033[38;5;146m
|
||||
C_335 = \033[38;5;147m
|
||||
C_340 = \033[38;5;148m
|
||||
C_341 = \033[38;5;149m
|
||||
C_342 = \033[38;5;150m
|
||||
C_343 = \033[38;5;151m
|
||||
C_344 = \033[38;5;152m
|
||||
C_345 = \033[38;5;153m
|
||||
C_350 = \033[38;5;154m
|
||||
C_351 = \033[38;5;155m
|
||||
C_352 = \033[38;5;156m
|
||||
C_353 = \033[38;5;157m
|
||||
C_354 = \033[38;5;158m
|
||||
C_355 = \033[38;5;159m
|
||||
C_400 = \033[38;5;160m
|
||||
C_401 = \033[38;5;161m
|
||||
C_402 = \033[38;5;162m
|
||||
C_403 = \033[38;5;163m
|
||||
C_404 = \033[38;5;164m
|
||||
C_405 = \033[38;5;165m
|
||||
C_410 = \033[38;5;166m
|
||||
C_411 = \033[38;5;167m
|
||||
C_412 = \033[38;5;168m
|
||||
C_413 = \033[38;5;169m
|
||||
C_414 = \033[38;5;170m
|
||||
C_415 = \033[38;5;171m
|
||||
C_420 = \033[38;5;172m
|
||||
C_421 = \033[38;5;173m
|
||||
C_422 = \033[38;5;174m
|
||||
C_423 = \033[38;5;175m
|
||||
C_424 = \033[38;5;176m
|
||||
C_425 = \033[38;5;177m
|
||||
C_430 = \033[38;5;178m
|
||||
C_431 = \033[38;5;179m
|
||||
C_432 = \033[38;5;180m
|
||||
C_433 = \033[38;5;181m
|
||||
C_434 = \033[38;5;182m
|
||||
C_435 = \033[38;5;183m
|
||||
C_440 = \033[38;5;184m
|
||||
C_441 = \033[38;5;185m
|
||||
C_442 = \033[38;5;186m
|
||||
C_443 = \033[38;5;187m
|
||||
C_444 = \033[38;5;188m
|
||||
C_445 = \033[38;5;189m
|
||||
C_450 = \033[38;5;190m
|
||||
C_451 = \033[38;5;191m
|
||||
C_452 = \033[38;5;192m
|
||||
C_453 = \033[38;5;193m
|
||||
C_454 = \033[38;5;194m
|
||||
C_455 = \033[38;5;195m
|
||||
C_500 = \033[38;5;196m
|
||||
C_501 = \033[38;5;197m
|
||||
C_502 = \033[38;5;198m
|
||||
C_503 = \033[38;5;199m
|
||||
C_504 = \033[38;5;200m
|
||||
C_505 = \033[38;5;201m
|
||||
C_510 = \033[38;5;202m
|
||||
C_511 = \033[38;5;203m
|
||||
C_512 = \033[38;5;204m
|
||||
C_513 = \033[38;5;205m
|
||||
C_514 = \033[38;5;206m
|
||||
C_515 = \033[38;5;207m
|
||||
C_520 = \033[38;5;208m
|
||||
C_521 = \033[38;5;209m
|
||||
C_522 = \033[38;5;210m
|
||||
C_523 = \033[38;5;211m
|
||||
C_524 = \033[38;5;212m
|
||||
C_525 = \033[38;5;213m
|
||||
C_530 = \033[38;5;214m
|
||||
C_531 = \033[38;5;215m
|
||||
C_532 = \033[38;5;216m
|
||||
C_533 = \033[38;5;217m
|
||||
C_534 = \033[38;5;218m
|
||||
C_535 = \033[38;5;219m
|
||||
C_540 = \033[38;5;220m
|
||||
C_541 = \033[38;5;221m
|
||||
C_542 = \033[38;5;222m
|
||||
C_543 = \033[38;5;223m
|
||||
C_544 = \033[38;5;224m
|
||||
C_545 = \033[38;5;225m
|
||||
C_550 = \033[38;5;226m
|
||||
C_551 = \033[38;5;227m
|
||||
C_552 = \033[38;5;228m
|
||||
C_553 = \033[38;5;229m
|
||||
C_554 = \033[38;5;230m
|
||||
C_555 = \033[38;5;231m
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
|
||||
INTRA_CLIENT_SECRET=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
|
||||
@@ -1,5 +1,5 @@
|
||||
volumes:
|
||||
data:
|
||||
pgdata:
|
||||
|
||||
networks:
|
||||
transcendence:
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- data:/var/lib/postgresql/data/pg15/
|
||||
- pgdata:/var/lib/postgresql
|
||||
env_file:
|
||||
- ../.env
|
||||
networks:
|
||||
@@ -38,8 +38,7 @@ services:
|
||||
container_name: frontend
|
||||
build: ./srcs/frontend/
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8443:8443"
|
||||
- "8443:443"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -127,7 +127,7 @@ async function createTables()
|
||||
status VARCHAR(20) DEFAULT 'waiting',
|
||||
max_players INT DEFAULT 8,
|
||||
current_round INT DEFAULT 0,
|
||||
max_rounds INT DEFAULT 3,
|
||||
max_rounds INT DEFAULT 5,
|
||||
round_duration INT DEFAULT 90,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
started_at TIMESTAMP,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
RUN mkdir -p /etc/backend/.ssl
|
||||
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/backend/.ssl/key.pem \
|
||||
-out /etc/backend/.ssl/cert.pem \
|
||||
-subj "/CN=localhost" \
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import cors from 'cors';
|
||||
import {Server} from 'socket.io';
|
||||
import authRouter from './routes/auth.js';
|
||||
@@ -11,9 +12,14 @@ import playerStatsRouter from './routes/player_stats.js';
|
||||
import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js';
|
||||
import setupSocketIO from './services/socket.js';
|
||||
import avatarService from './services/avatar.js';
|
||||
import intraRouter from './routes/intra.js';
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('/etc/backend/.ssl/key.pem'),
|
||||
cert: fs.readFileSync('/etc/backend/.ssl/cert.pem')
|
||||
};
|
||||
const server = https.createServer(httpsOptions, app);
|
||||
const io = new Server(server,
|
||||
{
|
||||
cors:
|
||||
@@ -48,6 +54,7 @@ async function startServer()
|
||||
app.use('/api/avatar', avatarRouter);
|
||||
app.use('/api/friends', friendsRouter);
|
||||
app.use('/api/stats', playerStatsRouter);
|
||||
app.use('/api/intra', intraRouter);
|
||||
app.get('/api', (req, res) => res.send('Backend running'));
|
||||
|
||||
server.listen(3001, () =>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// routes/intra.js
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
let token;
|
||||
|
||||
async function set_token() {
|
||||
try {
|
||||
const response = await fetch("https://api.intra.42.fr/oauth/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: process.env.INTRA_CLIENT_ID,
|
||||
client_secret: process.env.INTRA_CLIENT_SECRET
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
|
||||
token = await response.json();
|
||||
|
||||
setTimeout(set_token, (token.expires_in - 60) * 1000);
|
||||
} catch (e) {
|
||||
console.error("Token error:", e);
|
||||
}
|
||||
}
|
||||
set_token();
|
||||
|
||||
|
||||
router.get('/profile/:login', async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.intra.42.fr/v2/users/${req.params.login}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return res.status(response.status).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json(await response.json());
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to fetch profile' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -30,6 +30,63 @@ async function broadcastRoomsList(io) {
|
||||
}
|
||||
}
|
||||
|
||||
function startRoomTimer(io, roomId, seconds)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
if (gameState.timerInterval)
|
||||
clearInterval(gameState.timerInterval);
|
||||
|
||||
gameState.timerSeconds = seconds;
|
||||
|
||||
gameState.timerInterval = setInterval(() => {
|
||||
gameState.timerSeconds--;
|
||||
|
||||
if (gameState.timerSeconds < 0)
|
||||
gameState.timerSeconds = 0;
|
||||
|
||||
if (gameState.timerSeconds <= 0)
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: 0
|
||||
});
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
io.to(roomId).emit('game-timer-ended', { message: 'Temps écoulé !' });
|
||||
|
||||
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
||||
const nextDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = nextDrawer;
|
||||
|
||||
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
io.to(roomId).emit('game-new-round', {
|
||||
drawer: nextDrawer
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: gameState.timerSeconds
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopRoomTimer(roomId)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState || !gameState.timerInterval) return;
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
}
|
||||
|
||||
// Check if a playing game has only 1 player left and auto-stop it
|
||||
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
if (!dbRoomId) return;
|
||||
@@ -43,6 +100,7 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
||||
if (players.length <= 1) {
|
||||
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'ended'
|
||||
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
|
||||
@@ -82,6 +140,47 @@ async function saveRoundPoints(currentScores, roundStartScores) {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlayerDeparture(io, roomId, username) {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState || !gameState.isPlaying) return;
|
||||
if (!Array.isArray(gameState.players)) return;
|
||||
|
||||
const leavingIndex = gameState.players.indexOf(username);
|
||||
if (leavingIndex === -1) return;
|
||||
|
||||
const wasDrawer = gameState.drawer === username;
|
||||
|
||||
gameState.players = gameState.players.filter(p => p !== username);
|
||||
if (gameState.scores) {
|
||||
delete gameState.scores[username];
|
||||
}
|
||||
|
||||
if (gameState.currentPlayerIndex >= leavingIndex) {
|
||||
gameState.currentPlayerIndex = Math.max(0, gameState.currentPlayerIndex - 1);
|
||||
}
|
||||
if (gameState.currentPlayerIndex >= gameState.players.length) {
|
||||
gameState.currentPlayerIndex = 0;
|
||||
}
|
||||
|
||||
if (wasDrawer && gameState.players.length > 0) {
|
||||
stopRoomTimer(roomId);
|
||||
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = newDrawer;
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
io.to(roomId).emit('game-drawer-changed', {
|
||||
newDrawer: newDrawer,
|
||||
reason: 'drawer_left',
|
||||
message: `${username} (dessinateur) a quitte, ${newDrawer} devient le nouveau dessinateur`
|
||||
});
|
||||
startRoomTimer(io, roomId, 60);
|
||||
}
|
||||
}
|
||||
|
||||
function setupSocketIO(io)
|
||||
{
|
||||
ioInstance = io;
|
||||
@@ -192,7 +291,9 @@ function setupSocketIO(io)
|
||||
revealedLetters: gameState.revealedLetters,
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -202,11 +303,21 @@ function setupSocketIO(io)
|
||||
if (socket.gameRoomId) {
|
||||
const roomId = socket.gameRoomId;
|
||||
const dbRoomId = socket.gameRoomDbId;
|
||||
const userId = socket.user.userId;
|
||||
|
||||
if (dbRoomId && userId) {
|
||||
try {
|
||||
await gameRoomService.leaveRoom(dbRoomId, userId);
|
||||
} catch (err) {
|
||||
console.error('Error removing player from room on socket leave:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
userId: socket.user.userId
|
||||
});
|
||||
handlePlayerDeparture(io, roomId, socket.user.username);
|
||||
socket.leave(roomId);
|
||||
console.log(`${socket.user.username} left ${roomId}`);
|
||||
|
||||
@@ -268,7 +379,8 @@ function setupSocketIO(io)
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {}
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -390,6 +502,7 @@ function setupSocketIO(io)
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
startRoomTimer(io, roomId, 60);
|
||||
gameState.currentWord = data.word.toLowerCase();
|
||||
gameState.revealedLetters = new Array(data.word.length).fill(false);
|
||||
gameState.revealedWord = new Array(data.word.length).fill('_');
|
||||
@@ -503,7 +616,7 @@ function setupSocketIO(io)
|
||||
|
||||
// Points: 10 per letter found, -5 for wrong guess
|
||||
if (success) {
|
||||
points = lettersFound * 10;
|
||||
points = lettersFound * 5;
|
||||
gameState.scores[username] += points;
|
||||
} else {
|
||||
points = -5;
|
||||
@@ -552,6 +665,8 @@ function setupSocketIO(io)
|
||||
// Update round start scores for next round
|
||||
gameState.roundStartScores = { ...gameState.scores };
|
||||
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
io.to(roomId).emit('game-word-found', {
|
||||
word: gameState.currentWord,
|
||||
winner: username,
|
||||
@@ -600,40 +715,7 @@ function setupSocketIO(io)
|
||||
message: `${username} a quitté la partie`
|
||||
});
|
||||
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (gameState)
|
||||
{
|
||||
const wasDrawer = gameState.drawer === username;
|
||||
|
||||
gameState.players = gameState.players.filter(p => p !== username);
|
||||
delete gameState.scores[username];
|
||||
|
||||
io.to(roomId).emit('scores-updated', gameState.scores);
|
||||
|
||||
// If the drawer left and there are still enough players, choose a new drawer
|
||||
if (wasDrawer && gameState.players.length >= 1)
|
||||
{
|
||||
// Pick the next player as the new drawer
|
||||
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
|
||||
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = newDrawer;
|
||||
|
||||
// Reset the word state for the new round
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
console.log(`Drawer ${username} left, new drawer is ${newDrawer}`);
|
||||
|
||||
io.to(roomId).emit('game-drawer-changed', {
|
||||
newDrawer: newDrawer,
|
||||
reason: 'drawer_left',
|
||||
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
|
||||
});
|
||||
}
|
||||
}
|
||||
handlePlayerDeparture(io, roomId, username);
|
||||
|
||||
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
|
||||
|
||||
@@ -652,6 +734,7 @@ function setupSocketIO(io)
|
||||
socket.on('game-end', async () => {
|
||||
const roomId = socket.gameRoomId;
|
||||
if (!roomId) return;
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'waiting' in database
|
||||
const dbRoomId = socket.gameRoomDbId;
|
||||
@@ -867,11 +950,20 @@ function setupSocketIO(io)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dbRoomId && socket.user.userId) {
|
||||
try {
|
||||
await gameRoomService.leaveRoom(dbRoomId, socket.user.userId);
|
||||
} catch (err) {
|
||||
console.error('Error removing disconnected player from room:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular player disconnect
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
userId: socket.user.userId
|
||||
});
|
||||
handlePlayerDeparture(io, roomId, socket.user.username);
|
||||
|
||||
// Get updated player list and broadcast
|
||||
if (dbRoomId) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk add --no-cache openssl && \
|
||||
mkdir -p /etc/nginx/ssl && \
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
@@ -6,7 +7,14 @@ RUN apk add --no-cache openssl && \
|
||||
-out /etc/nginx/ssl/cert.pem \
|
||||
-subj "/CN=localhost" \
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
|
||||
#ADDED
|
||||
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY src /usr/share/nginx/html
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 8080 8443
|
||||
|
||||
EXPOSE 443
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,13 +1,9 @@
|
||||
server {
|
||||
listen 8080;
|
||||
return 301 https://$host:8443$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8443 ssl;
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
error_page 497 =301 https://$host:8443$request_uri;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -19,7 +15,8 @@ server {
|
||||
|
||||
# Backend API
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_pass https://backend:3001;
|
||||
proxy_ssl_verify off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
@@ -27,7 +24,8 @@ server {
|
||||
|
||||
# Socket.IO WebSocket proxying
|
||||
location /socket.io/ {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_pass https://backend:3001;
|
||||
proxy_ssl_verify off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -40,9 +38,10 @@ server {
|
||||
}
|
||||
|
||||
location /avatar/ {
|
||||
proxy_pass http://backend:3001/avatar/;
|
||||
proxy_pass https://backend:3001/avatar/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_ssl_verify off;
|
||||
proxy_hide_header Content-Type;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Initializes windows and handles menu interactions
|
||||
*/
|
||||
import { windowRegistry } from './core/windows.js';
|
||||
import { API, STORAGE_KEYS } from './core/config.js';
|
||||
import { eventBus, Events } from './core/events.js';
|
||||
import { LoginWindow } from './windows/login.js';
|
||||
import { LogoutWindow } from './windows/logout.js';
|
||||
import { GlobalChat } from './windows/global_chat.js';
|
||||
@@ -17,10 +19,57 @@ import { StatsWindow } from './windows/stats.js';
|
||||
*/
|
||||
class App {
|
||||
constructor() {
|
||||
this.invalidateStaleToken();
|
||||
this.initWindows();
|
||||
this.initMenu();
|
||||
this.initPage();
|
||||
this.initEasterEgg();
|
||||
this.colorizeUI();
|
||||
}
|
||||
|
||||
async invalidateStaleToken() {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) return;
|
||||
|
||||
if (this.isJwtExpired(token)) {
|
||||
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
eventBus.emit(Events.USER_LOGGED_OUT);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API.STATS.ME, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
eventBus.emit(Events.USER_LOGGED_OUT);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Token validation skipped:', error);
|
||||
}
|
||||
}
|
||||
|
||||
isJwtExpired(token) {
|
||||
try {
|
||||
const payload = this.decodeJwtPayload(token);
|
||||
if (!payload || !payload.exp) return false;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return payload.exp <= now;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decodeJwtPayload(token) {
|
||||
const parts = token.split('.');
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
||||
return JSON.parse(atob(padded));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,10 +126,6 @@ class App {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionMap = {
|
||||
'gameroom': 'gameroom'
|
||||
};
|
||||
|
||||
// Event delegation on the menu
|
||||
page.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('.page__item');
|
||||
@@ -88,9 +133,14 @@ class App {
|
||||
|
||||
const action = button.dataset.action;
|
||||
|
||||
// Actions with associated windows
|
||||
if (actionMap[action]) {
|
||||
windowRegistry.toggle(actionMap[action]);
|
||||
if (action === 'gameroom') {
|
||||
const gameRoomWindow = windowRegistry.get('gameroom');
|
||||
windowRegistry.toggle('gameroom');
|
||||
gameRoomWindow.loadRooms();
|
||||
|
||||
if (gameRoomWindow?.currentTab === 'browse') {
|
||||
gameRoomWindow.loadRooms();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,6 +158,39 @@ class App {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
colorizeUI() {
|
||||
|
||||
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
|
||||
|
||||
const colorizeText = (el) => {
|
||||
const text = el.textContent;
|
||||
el.innerHTML = "";
|
||||
|
||||
const baseHue = Math.random() * 360;
|
||||
|
||||
// 🎲 random step = makes rainbow "scrambled"
|
||||
const step = (Math.random() * 60) + 10; // 10 → 70
|
||||
|
||||
// 🎲 random direction (left or right rainbow)
|
||||
const direction = Math.random() < 0.5 ? 1 : -1;
|
||||
|
||||
[...text].forEach((char, i) => {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = char;
|
||||
|
||||
const hue = baseHue + (i * step * direction);
|
||||
|
||||
span.style.color = `hsl(${hue}, 90%, 60%)`;
|
||||
|
||||
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
|
||||
|
||||
el.appendChild(span);
|
||||
});
|
||||
};
|
||||
elements.forEach(colorizeText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Start the application when DOM is ready
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,87 @@
|
||||
|
||||
.shape {
|
||||
/* The "Physical" properties */
|
||||
position: fixed;
|
||||
/* transform: translate(-50%, -50%); Optional: This makes 'left/top' refer to the CENTER of the doodle */
|
||||
|
||||
width: 142px;
|
||||
height: 142px;
|
||||
|
||||
/* The "Stenciling" instructions (but no image yet!) */
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
/* The default "Paint" color */
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.shape:hover {
|
||||
transform: scale(1.2); /* Grow by 20% when you hover the mouse over it */
|
||||
transition: transform 0.3s ease; /* Make it a smooth grow */
|
||||
}
|
||||
|
||||
/* Individual Doodle Definitions */
|
||||
.doodle-0 { -webkit-mask-image: url('doodles/cat.png'); mask-image: url('doodles/cat.png'); left: 60vw; top: 35vh; }
|
||||
.doodle-1 { -webkit-mask-image: url('doodles/ball.png'); mask-image: url('doodles/ball.png'); left: 10vw; top: 10vh; }
|
||||
.doodle-2 { -webkit-mask-image: url('doodles/batman.png'); mask-image: url('doodles/batman.png'); left: 20vw; top: 15vh; }
|
||||
.doodle-3 { -webkit-mask-image: url('doodles/building.png'); mask-image: url('doodles/building.png'); left: 30vw; top: 20vh; }
|
||||
.doodle-4 { -webkit-mask-image: url('doodles/butterfly.png'); mask-image: url('doodles/butterfly.png'); left: 40vw; top: 25vh; }
|
||||
.doodle-5 { -webkit-mask-image: url('doodles/car.png'); mask-image: url('doodles/car.png'); left: 50vw; top: 30vh; }
|
||||
.doodle-6 { -webkit-mask-image: url('doodles/yin_yang.png'); mask-image: url('doodles/yin_yang.png'); left: 88vw; top: 12vh; }
|
||||
.doodle-7 { -webkit-mask-image: url('doodles/clouds.png'); mask-image: url('doodles/clouds.png'); left: 70vw; top: 40vh; }
|
||||
.doodle-8 { -webkit-mask-image: url('doodles/controls.png'); mask-image: url('doodles/controls.png'); left: 80vw; top: 45vh; }
|
||||
.doodle-9 { -webkit-mask-image: url('doodles/dead.png'); mask-image: url('doodles/dead.png'); left: 90vw; top: 50vh; }
|
||||
.doodle-10 { -webkit-mask-image: url('doodles/diamant.png'); mask-image: url('doodles/diamant.png'); left: 15vw; top: 55vh; }
|
||||
.doodle-11 { -webkit-mask-image: url('doodles/dice.png'); mask-image: url('doodles/dice.png'); left: 25vw; top: 60vh; }
|
||||
.doodle-12 { -webkit-mask-image: url('doodles/earth.png'); mask-image: url('doodles/earth.png'); left: 35vw; top: 65vh; }
|
||||
.doodle-13 { -webkit-mask-image: url('doodles/egypt.png'); mask-image: url('doodles/egypt.png'); left: 45vw; top: 70vh; }
|
||||
.doodle-14 { -webkit-mask-image: url('doodles/fire.png'); mask-image: url('doodles/fire.png'); left: 55vw; top: 75vh; }
|
||||
.doodle-15 { -webkit-mask-image: url('doodles/fish.png'); mask-image: url('doodles/fish.png'); left: 65vw; top: 80vh; }
|
||||
.doodle-16 { -webkit-mask-image: url('doodles/flag.png'); mask-image: url('doodles/flag.png'); left: 75vw; top: 85vh; }
|
||||
.doodle-17 { -webkit-mask-image: url('doodles/hearts.png'); mask-image: url('doodles/hearts.png'); left: 85vw; top: 90vh; }
|
||||
.doodle-18 { -webkit-mask-image: url('doodles/house.png'); mask-image: url('doodles/house.png'); left: 5vw; top: 45vh; }
|
||||
.doodle-19 { -webkit-mask-image: url('doodles/idol.png'); mask-image: url('doodles/idol.png'); left: 12vw; top: 22vh; }
|
||||
.doodle-20 { -webkit-mask-image: url('doodles/lotus.png'); mask-image: url('doodles/lotus.png'); left: 22vw; top: 32vh; }
|
||||
.doodle-21 { -webkit-mask-image: url('doodles/mail.png'); mask-image: url('doodles/mail.png'); left: 32vw; top: 42vh; }
|
||||
.doodle-22 { -webkit-mask-image: url('doodles/moon.png'); mask-image: url('doodles/moon.png'); left: 42vw; top: 52vh; }
|
||||
.doodle-23 { -webkit-mask-image: url('doodles/pokeball.png'); mask-image: url('doodles/pokeball.png'); left: 52vw; top: 62vh; }
|
||||
.doodle-24 { -webkit-mask-image: url('doodles/runes.png'); mask-image: url('doodles/runes.png'); left: 62vw; top: 72vh; }
|
||||
.doodle-25 { -webkit-mask-image: url('doodles/shield.png'); mask-image: url('doodles/shield.png'); left: 72vw; top: 82vh; }
|
||||
.doodle-26 { -webkit-mask-image: url('doodles/shiny.png'); mask-image: url('doodles/shiny.png'); left: 82vw; top: 12vh; }
|
||||
.doodle-27 { -webkit-mask-image: url('doodles/snail.png'); mask-image: url('doodles/snail.png'); left: 92vw; top: 22vh; }
|
||||
.doodle-28 { -webkit-mask-image: url('doodles/sound.png'); mask-image: url('doodles/sound.png'); left: 18vw; top: 82vh; }
|
||||
.doodle-29 { -webkit-mask-image: url('doodles/spiral.png'); mask-image: url('doodles/spiral.png'); left: 28vw; top: 72vh; }
|
||||
.doodle-30 { -webkit-mask-image: url('doodles/star.png'); mask-image: url('doodles/star.png'); left: 38vw; top: 62vh; }
|
||||
.doodle-31 { -webkit-mask-image: url('doodles/stop.png'); mask-image: url('doodles/stop.png'); left: 48vw; top: 52vh; }
|
||||
.doodle-32 { -webkit-mask-image: url('doodles/sun.png'); mask-image: url('doodles/sun.png'); left: 58vw; top: 42vh; }
|
||||
.doodle-33 { -webkit-mask-image: url('doodles/tree.png'); mask-image: url('doodles/tree.png'); left: 68vw; top: 32vh; }
|
||||
.doodle-34 { -webkit-mask-image: url('doodles/triskel.png'); mask-image: url('doodles/triskel.png'); left: 78vw; top: 22vh; }
|
||||
|
||||
|
||||
/* 3. A quick animation for the color loop */
|
||||
.loop-color {
|
||||
animation: colorShift 12s infinite alternate ease-in-out;
|
||||
}
|
||||
@keyframes colorShift {
|
||||
/* 0% and 100% are identical to create the "Infinite Circle" effect */
|
||||
0% { background-color: #3075ff; } /* Royal Blue (Start) */
|
||||
|
||||
8% { background-color: #24a1ff; } /* Sky Blue */
|
||||
17% { background-color: #1ad8ff; } /* Cyan */
|
||||
|
||||
25% { background-color: #1bffa7; } /* Seafoam Green */
|
||||
33% { background-color: #1fff4d; } /* Bright Green */
|
||||
42% { background-color: #8bff32; } /* Lime Green */
|
||||
|
||||
50% { background-color: #dcff38; } /* Electric Yellow */
|
||||
58% { background-color: #ffbc29; } /* Golden Yellow */
|
||||
67% { background-color: #ff8c4a; } /* Coral Orange */
|
||||
|
||||
75% { background-color: #ff1d1d; } /* Hot Red */
|
||||
83% { background-color: #ff2bf3; } /* Magenta Pink */
|
||||
92% { background-color: #ac37ff; } /* Electric Purple */
|
||||
|
||||
100% { background-color: #3075ff; } /* Royal Blue (Seamless Loop) */
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
const maxdoodles = 34;
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////>\
|
||||
// container for all doodles, create them
|
||||
class DoodleContainer {
|
||||
|
||||
constructor(parent) {
|
||||
|
||||
this.parent = parent;
|
||||
this.obj = document.createElement('div');
|
||||
Object.assign(this.obj.style, {
|
||||
width: '100vw',
|
||||
height: '100vw',
|
||||
});
|
||||
|
||||
this.createAllDoodles();
|
||||
parent.append(this.obj);
|
||||
this.randomizeAnimationStarts();
|
||||
}
|
||||
|
||||
createAllDoodles() {
|
||||
|
||||
for (let i = 0; i <= maxdoodles; i++) {
|
||||
let d = document.createElement('div');
|
||||
d.classList.add('shape', 'doodle-' + i, 'loop-color');
|
||||
d.id = 'shape' + i;
|
||||
this.obj.append(d);
|
||||
d.addEventListener('click', () => {
|
||||
console.log(`hi from ${d.id}!`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
startSmoothRandomMove(id, speed = 2) {
|
||||
|
||||
const el = document.getElementById(id);
|
||||
if (!el)
|
||||
return;
|
||||
|
||||
// 1. Get initial pixel position or pick random if CSS isn't loaded yet
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
const state = {
|
||||
x: rect.left || Math.random() * (window.innerWidth - 142),
|
||||
y: rect.top || Math.random() * (window.innerHeight - 142),
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
speed: speed
|
||||
};
|
||||
|
||||
function update() {
|
||||
// 2. Refresh screen boundaries every frame
|
||||
const screenW = window.innerWidth;
|
||||
const screenH = window.innerHeight;
|
||||
const shapeSize = 142; // Matches your CSS width/height
|
||||
|
||||
// 3. Calculate next step
|
||||
state.x += Math.cos(state.angle) * state.speed;
|
||||
state.y += Math.sin(state.angle) * state.speed;
|
||||
|
||||
// 4. BOUNCE LOGIC
|
||||
// Horizontal check
|
||||
if (state.x <= 0) {
|
||||
state.x = 0;
|
||||
state.angle = Math.PI - state.angle;
|
||||
} else if (state.x + shapeSize >= screenW) {
|
||||
state.x = screenW - shapeSize;
|
||||
state.angle = Math.PI - state.angle;
|
||||
}
|
||||
|
||||
// Vertical check
|
||||
if (state.y <= 0) {
|
||||
state.y = 0;
|
||||
state.angle = -state.angle;
|
||||
} else if (state.y + shapeSize >= screenH) {
|
||||
state.y = screenH - shapeSize;
|
||||
state.angle = -state.angle;
|
||||
}
|
||||
|
||||
// 5. Apply position using pixels for precision
|
||||
el.style.left = state.x + "px";
|
||||
el.style.top = state.y + "px";
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
randomizeAnimationStarts() {
|
||||
for (let i = 0; i <= maxdoodles; i++) {
|
||||
const randomSpeed = 1 + Math.random() * 3;
|
||||
this.startSmoothRandomMove(`shape${i}`, randomSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////>
|
||||
// all loop-color have the same @colorShift animation cycle, this disynchronize them
|
||||
function randomizeColorsStarts() {
|
||||
const shapes = document.querySelectorAll('.loop-color');
|
||||
|
||||
shapes.forEach(shape => {
|
||||
// Pick a random number between 0 and 10 (since your loop is 10s)
|
||||
const randomDelay = Math.random() * - 15;
|
||||
|
||||
// Apply it directly to the element's style
|
||||
shape.style.animationDelay = randomDelay + "s";
|
||||
});
|
||||
}
|
||||
|
||||
const a = new DoodleContainer(document.body);
|
||||
// Call this once when the script loads
|
||||
randomizeColorsStarts();
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 994 B |
|
After Width: | Height: | Size: 1018 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 955 B |
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1000 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,26 +1,25 @@
|
||||
:root {
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-hover: #0052a3;
|
||||
--color-primary: #ffc75e;
|
||||
--color-primary-hover: #ffc75e;
|
||||
--color-success: #3cff01;
|
||||
--color-success-dark: #28a745;
|
||||
--color-success-dark: #ffc75e;
|
||||
--color-error: #ff4d4d;
|
||||
--color-warning: #ffc107;
|
||||
--color-github: #24292e;
|
||||
--color-warning: #ffc75e;
|
||||
--color-github: #ffc75e;
|
||||
|
||||
--color-bg: #000;
|
||||
--color-bg: #ffe5b5;
|
||||
|
||||
--app-background-base: radial-gradient(
|
||||
circle at top,
|
||||
#1b2735,
|
||||
#090a0f
|
||||
#3fc9ff,
|
||||
#21fcc5
|
||||
|
||||
);
|
||||
|
||||
/* --app-background-image: url("./assets/background.png"); */
|
||||
|
||||
--color-surface: #222;
|
||||
--color-surface-light: #333;
|
||||
--color-text: #fff;
|
||||
--color-text-muted: #aaa;
|
||||
--color-surface: #ffcc00;
|
||||
--color-surface-light: #feffa6;
|
||||
--color-text: #000000;
|
||||
--color-text-muted: #353535;
|
||||
|
||||
--font-size-base: 10px;
|
||||
--font-size-sm: 1.2rem;
|
||||
@@ -62,24 +61,28 @@
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
background-image:
|
||||
background-image:
|
||||
var(--app-background-base);
|
||||
|
||||
background-size: contain, cover;
|
||||
background-position: center, center;
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
|
||||
|
||||
background-size:
|
||||
contain,
|
||||
cover;
|
||||
|
||||
background-position:
|
||||
center,
|
||||
center;
|
||||
|
||||
background-repeat:
|
||||
no-repeat,
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
@@ -93,102 +96,69 @@ body {
|
||||
}
|
||||
|
||||
|
||||
/* ============================================
|
||||
ANIMATIONS
|
||||
============================================ */
|
||||
|
||||
@keyframes wobble {
|
||||
0% { transform: translate(0%, 0) rotate(0deg); }
|
||||
25% { transform: translate(-5%, -1px) rotate(-0.5deg); }
|
||||
50% { transform: translate(0%, 1px) rotate(0.5deg); }
|
||||
75% { transform: translate(+5%, -1px) rotate(0.5deg); }
|
||||
100% { transform: translate(0%, 0) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% { transform: translateY(0) rotate(var(--rot)); }
|
||||
33% { transform: translateY(-6px) rotate(var(--rot)); }
|
||||
66% { transform: translateY(-8px) rotate(var(--rot)); }
|
||||
100% { transform: translateY(0) rotate(var(--rot)); }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
font-size: var(--font-size-xl);
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 10px black;
|
||||
z-index: 1;
|
||||
font-family: "Cinzel Decorative", cursive;
|
||||
color: var(--color-success);
|
||||
margin: 0;
|
||||
padding: var(--spacing-md);
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
background: #ffcc00;
|
||||
color: #000;
|
||||
|
||||
border: 4px solid #feffa6;
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 0.6rem 1.2rem;
|
||||
|
||||
animation: wobble 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MENU
|
||||
============================================ */
|
||||
.title span {
|
||||
display: inline-block;
|
||||
transform-origin: center;
|
||||
font-size: 4rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5);
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: var(--z-menu);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
animation: bounce 1.2s infinite alternate;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: left;
|
||||
}
|
||||
.title span:nth-child(1) { --rot: -5deg; color: #ff4d4d; }
|
||||
.title span:nth-child(2) { --rot: 3deg; color: #5beb67; }
|
||||
.title span:nth-child(3) { --rot: -3deg; color: #ca8dfc; }
|
||||
.title span:nth-child(4) { --rot: 2deg; color: #6698f5; }
|
||||
.title span:nth-child(5) { --rot: -4deg; color: #ff66cc; }
|
||||
|
||||
.menu__item:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
.title span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.title span:nth-child(3) { animation-delay: 0.4s; }
|
||||
.title span:nth-child(4) { animation-delay: 0.6s; }
|
||||
.title span:nth-child(5) { animation-delay: 0.8s; }
|
||||
|
||||
.menu__item--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
GAME
|
||||
============================================ */
|
||||
|
||||
.game {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 50px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: var(--z-menu);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.game__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.game__item:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.game__item--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.title span { will-change: transform; }
|
||||
|
||||
/* ============================================
|
||||
PAGES
|
||||
@@ -208,14 +178,17 @@ body {
|
||||
}
|
||||
|
||||
.page__item {
|
||||
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border-color: #fda725;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page__item:hover {
|
||||
@@ -229,9 +202,87 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
MENU
|
||||
============================================ */
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
top: var(--spacing-lg);
|
||||
left: 50px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
z-index: var(--z-menu);
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #fda725;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu__item:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.menu__item--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
GAME
|
||||
============================================ */
|
||||
|
||||
|
||||
.game {
|
||||
position: fixed;
|
||||
top: var(--spacing-lg);
|
||||
right: 50px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
z-index: var(--z-menu);
|
||||
}
|
||||
|
||||
.game__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #fda725;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game__item:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.game__item--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -256,7 +307,7 @@ body {
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -265,7 +316,7 @@ body {
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
background: var(--color-surface-light);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -328,13 +379,15 @@ body {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-bg);
|
||||
border: 2px ridge var(--color-text);
|
||||
color: var(--color-text);
|
||||
z-index: var(--z-window);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
min-width: 280px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-radius: 5px;
|
||||
border-color: #aa1f1f;
|
||||
border: 6px solid #faac37;
|
||||
}
|
||||
|
||||
.window--visible {
|
||||
@@ -395,7 +448,8 @@ body {
|
||||
.message {
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.message--success {
|
||||
@@ -415,6 +469,11 @@ body {
|
||||
============================================ */
|
||||
.login {
|
||||
width: 320px;
|
||||
border-radius: 5px;
|
||||
border-color: #aa1f1f;
|
||||
border: 6px solid #faac37;
|
||||
background: #ffffff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.login__form {
|
||||
@@ -533,7 +592,7 @@ body {
|
||||
border-radius: var(--radius-full);
|
||||
border: 3px solid var(--color-text);
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--color-surface);
|
||||
background: var(--color-surface-light);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -557,28 +616,74 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
EASTER EGG BUTTON
|
||||
STATS WINDOW
|
||||
============================================ */
|
||||
/* .easter-egg {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-md);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
.stats-window {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.easter-egg:hover {
|
||||
background: var(--color-error);
|
||||
border-color: var(--color-error);
|
||||
} */
|
||||
.stats__avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-full);
|
||||
border: 2px solid var(--color-text);
|
||||
align-self: center;
|
||||
display: block;
|
||||
margin: 0 auto var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stats__username {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.stats__section {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.stats__section-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-primary);
|
||||
border-bottom: 1px solid var(--color-surface-light);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stats__section-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stats__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.stats__label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats__value {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.stats__loading {
|
||||
font-size: var(--font-size-sm);
|
||||
color: #333;
|
||||
text-align: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
@@ -626,7 +731,7 @@ body {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border: 1px solid var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
@@ -709,9 +814,10 @@ body {
|
||||
/* ============================================
|
||||
GAME ROOM WINDOW
|
||||
============================================ */
|
||||
|
||||
.gameroom-window {
|
||||
width: 600px;
|
||||
height: 800px;
|
||||
width: 800px;
|
||||
height: 900px;
|
||||
}
|
||||
|
||||
.gameroom__tabs {
|
||||
@@ -1019,3 +1125,4 @@ body {
|
||||
.gameroom__game-buttons .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Lobby</title>
|
||||
<title>Skkribl.io</title>
|
||||
<link rel="stylesheet" href="doodle.css">
|
||||
<link rel="stylesheet" href="game.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<script src="doodle.js" defer></script>
|
||||
<script type="module" src="../app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="title">Lobby</h1>
|
||||
<h1 class="title">
|
||||
<span>L</span>
|
||||
<span>o</span>
|
||||
<span>b</span>
|
||||
<span>b</span>
|
||||
<span>y</span>
|
||||
</h1>
|
||||
|
||||
<nav class="menu" aria-label="Menu principal">
|
||||
<button class="menu__item" data-action="login" aria-label="Login">Login</button>
|
||||
@@ -29,6 +39,5 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script type="module" src="../app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,28 +7,28 @@
|
||||
CSS VARIABLES
|
||||
============================================ */
|
||||
:root {
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-hover: #0052a3;
|
||||
--color-primary: #ffc75e;
|
||||
--color-primary-hover: #ffc75e;
|
||||
--color-success: #3cff01;
|
||||
--color-success-dark: #28a745;
|
||||
--color-success-dark: #ffc75e;
|
||||
--color-error: #ff4d4d;
|
||||
--color-warning: #ffc107;
|
||||
--color-github: #24292e;
|
||||
--color-warning: #ffc75e;
|
||||
--color-github: #ffc75e;
|
||||
|
||||
--color-bg: #a3a3a3;
|
||||
--color-bg: #ffe5b5;
|
||||
|
||||
--app-background-base: radial-gradient(
|
||||
circle at top,
|
||||
#000000,
|
||||
#4d4d4d
|
||||
#fff787,
|
||||
#ff8080
|
||||
);
|
||||
|
||||
--app-background-image: url("./assets/background.png");
|
||||
|
||||
--color-surface: #222;
|
||||
--color-surface-light: #333;
|
||||
--color-text: #fff;
|
||||
--color-text-muted: #aaa;
|
||||
--color-surface: #ffefce;
|
||||
--color-surface-light: #ffc75e;
|
||||
--color-text: #000000;
|
||||
--color-text-muted: #000000;
|
||||
|
||||
--font-size-base: 10px;
|
||||
--font-size-sm: 1.2rem;
|
||||
@@ -69,32 +69,45 @@
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
background-image: var(--app-background-image), var(--app-background-base);
|
||||
|
||||
/* Make background image responsive but limited in size */
|
||||
background-size: clamp(400px, 100%, 800px) auto, cover;
|
||||
|
||||
/* Align image to the top, gradient stays centered */
|
||||
background-position: center 35%, center;
|
||||
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
}
|
||||
|
||||
/* html {
|
||||
height: 100%;
|
||||
background-image:
|
||||
var(--app-background-image),
|
||||
var(--app-background-base);
|
||||
|
||||
background-size:
|
||||
contain,
|
||||
80%,
|
||||
cover;
|
||||
|
||||
background-position:
|
||||
center,
|
||||
50% 20%,
|
||||
center;
|
||||
|
||||
background-repeat:
|
||||
no-repeat,
|
||||
no-repeat;
|
||||
}
|
||||
} */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
min-width: 800px;
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh; /* KEY */
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -117,16 +130,16 @@ body {
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 10px black;
|
||||
z-index: 1;
|
||||
font-family: "Cinzel Decorative", cursive;
|
||||
font-family: "Roboto";
|
||||
letter-spacing: -10px;
|
||||
color: rgba(248, 252, 2, 0.6);
|
||||
|
||||
margin: 0;
|
||||
padding: var(--spacing-md);
|
||||
padding: 0.6rem 1.2rem;
|
||||
|
||||
/* Rectangle + rounded corners */
|
||||
background-color: rgba(247, 7, 67, 0.6);
|
||||
background-color: #ffefce;
|
||||
border: 2px solid rgba(0, 0, 0, 0.6);
|
||||
border-radius: 15px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,25 +149,27 @@ body {
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: var(--spacing-lg);
|
||||
left: 50px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: var(--z-menu);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
z-index: var(--z-menu);
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #000;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu__item:hover {
|
||||
@@ -171,7 +186,7 @@ body {
|
||||
GAME
|
||||
============================================ */
|
||||
|
||||
.game {
|
||||
/* .game {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 50px;
|
||||
@@ -181,17 +196,31 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
} */
|
||||
|
||||
.game {
|
||||
position: fixed;
|
||||
top: var(--spacing-lg);
|
||||
right: 50px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
z-index: var(--z-menu);
|
||||
}
|
||||
|
||||
.game__item {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-surface-light);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #000;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game__item:hover {
|
||||
@@ -204,6 +233,57 @@ body {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ///////////////////////////////////////////////////////// */
|
||||
|
||||
footer {
|
||||
margin-top: auto; /* pushes footer to bottom */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer_div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30%; /* space between items */
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.footer_grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
justify-items: center; /* center each column */
|
||||
align-items: center;
|
||||
gap: 10px 60px; /* row gap / column gap */
|
||||
}
|
||||
|
||||
.ico_footer {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000000;
|
||||
}
|
||||
a:hover {
|
||||
color: rgb(218, 145, 12);
|
||||
}
|
||||
|
||||
.legal {
|
||||
margin-top: 5%;
|
||||
padding-bottom: 5%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.legal:hover {
|
||||
color: rgb(218, 145, 12);
|
||||
}
|
||||
|
||||
/* ///////////////////////////////////////////////////////// */
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
@@ -303,13 +383,15 @@ body {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-bg);
|
||||
border: 2px ridge var(--color-text);
|
||||
color: var(--color-text);
|
||||
z-index: var(--z-window);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
min-width: 280px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-radius: 5px;
|
||||
border-color: #aa1f1f;
|
||||
border: 6px solid #faac37;
|
||||
}
|
||||
|
||||
.window--visible {
|
||||
@@ -370,7 +452,8 @@ body {
|
||||
.message {
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.message--success {
|
||||
@@ -750,3 +833,42 @@ body {
|
||||
color: var(--color-text-muted);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.container-1 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* ///////////////////////////////////////////////////////// */
|
||||
|
||||
.button {
|
||||
color: red;
|
||||
margin: 5px 50px;
|
||||
padding: 5px 50px;
|
||||
}
|
||||
|
||||
.button-1 {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #000000;
|
||||
color: #8e8e8e;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border: 3px solid #363636;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
}
|
||||
.button-1:hover {
|
||||
background-color: rgb(202, 135, 10);
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Transcendence.io</title>
|
||||
<title>Transcendence</title>
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="title">Transcendence.io</h1>
|
||||
<h1 class="title">Transcendence</h1>
|
||||
|
||||
<nav class="menu" aria-label="Menu principal">
|
||||
<button class="menu__item" data-action="login" aria-label="Login">Login</button>
|
||||
@@ -21,12 +21,30 @@
|
||||
</nav>
|
||||
|
||||
<nav class="game" aria-label="Game">
|
||||
<button class="game__item" data-action="new_game" aria-label="Start new game"
|
||||
onclick="window.location.href='game/game.html'">Start new game</button>
|
||||
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io"
|
||||
onclick="window.location.href='./game/game.html'">Skkrrribl.io</button>
|
||||
<button class="game__item" data-action="tetris" aria-label="Tetris"
|
||||
onclick="window.location.href='tetris/tetris.html'">Tetris</button>
|
||||
onclick="window.location.href='./tetris/tetris.html'">Tetris</button>
|
||||
<button class="game__item" data-action="Wiskas" aria-label="Wiskas"
|
||||
onclick="window.location.href='./wiskas/wiskas.html'">Wiskas</button>
|
||||
</nav>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
<script type="module" src="./app.js"></script>
|
||||
<script type="module" src="./script.js"></script>
|
||||
|
||||
<footer>
|
||||
<div class="footer_grid">
|
||||
<img class="ico_footer" src="./assets/facebook_logo.png">
|
||||
<img class="ico_footer" src="./assets/insta_logo.png">
|
||||
<img class="ico_footer" src="./assets/twitter_logo.png">
|
||||
|
||||
<a href="https://www.facebook.com/">MIAOUBOOK</a>
|
||||
<a href="https://www.instagram.com/">INSTAMIAOU</a>
|
||||
<a href="https://twitter.com/">BLUE-SNACK</a>
|
||||
</div>
|
||||
|
||||
<a href="./mentions/mentions_legales.html" class="legal">LEGAL NOTICES</a>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Legal Notices</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #fff8e1;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.legal {
|
||||
max-width: 600px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-home {
|
||||
padding: 10px 20px;
|
||||
background-color: #ffc75e;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn-home:hover {
|
||||
background-color: #ffb347;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="legal">
|
||||
<strong>Legal Notices</strong><br><br>
|
||||
All user credentials are securely stored using hashed passwords. We respect your privacy and do not share your personal information with third parties. By using this site, you agree to our data handling practices.
|
||||
</div>
|
||||
<a href="../index.html" class="btn-home">Back to Home</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
const container = document.querySelector('.container-gamelinks');
|
||||
const buttons = document.querySelectorAll('.game-button');
|
||||
|
||||
function initButtons() {
|
||||
const rect = container.getBoundingClientRect();
|
||||
|
||||
buttons.forEach(btn => {
|
||||
// Ensure size is known
|
||||
const bw = btn.offsetWidth;
|
||||
const bh = btn.offsetHeight;
|
||||
|
||||
// Random start position INSIDE container
|
||||
btn.x = Math.random() * (rect.width - bw);
|
||||
btn.y = Math.random() * (rect.height - bh);
|
||||
|
||||
// Better velocity (avoid super slow)
|
||||
btn.vx = (Math.random() * 2 + 1) * (Math.random() < 0.5 ? -1 : 1);
|
||||
btn.vy = (Math.random() * 2 + 1) * (Math.random() < 0.5 ? -1 : 1);
|
||||
|
||||
btn.style.left = btn.x + 'px';
|
||||
btn.style.top = btn.y + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function animateButtons() {
|
||||
const rect = container.getBoundingClientRect();
|
||||
|
||||
buttons.forEach(btn => {
|
||||
btn.x += btn.vx;
|
||||
btn.y += btn.vy;
|
||||
|
||||
const bw = btn.offsetWidth;
|
||||
const bh = btn.offsetHeight;
|
||||
|
||||
// Bounce inside container
|
||||
if (btn.x <= 0 || btn.x + bw >= rect.width) {
|
||||
btn.vx *= -1;
|
||||
btn.x = Math.max(0, Math.min(btn.x, rect.width - bw)); // clamp
|
||||
}
|
||||
|
||||
if (btn.y <= 0 || btn.y + bh >= rect.height) {
|
||||
btn.vy *= -1;
|
||||
btn.y = Math.max(0, Math.min(btn.y, rect.height - bh)); // clamp
|
||||
}
|
||||
|
||||
btn.style.left = btn.x + 'px';
|
||||
btn.style.top = btn.y + 'px';
|
||||
});
|
||||
|
||||
requestAnimationFrame(animateButtons);
|
||||
}
|
||||
|
||||
// 🔥 IMPORTANT: wait for layout to be ready
|
||||
window.addEventListener('load', () => {
|
||||
initButtons();
|
||||
animateButtons();
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TETRIS</title>
|
||||
<title>Tetris</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="tetris.css">
|
||||
</head>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<h1 data-text="TETRIS">TETRIS<span class="cursor">_</span></h1>
|
||||
|
||||
<a id="btn-home" href="/">Home</a>
|
||||
<a id="btn-home" href="../index.html">Home</a>
|
||||
|
||||
<!-- Panneau duel -->
|
||||
<div id="duel-panel">
|
||||
|
||||
@@ -34,6 +34,7 @@ export class GameRoomWindow extends Window {
|
||||
});
|
||||
|
||||
this.updateTabsAccess();
|
||||
this.loadCurrentTab();
|
||||
|
||||
// Verifier si l'utilisateur est deja dans un salon au chargement
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
@@ -119,7 +120,8 @@ export class GameRoomWindow extends Window {
|
||||
this.gameInfo = this.createElement('div', 'gameroom__game-info');
|
||||
this.currentDrawerInfo = this.createElement('div', 'gameroom__drawer-info', { text: '' });
|
||||
this.scoresDisplay = this.createElement('div', 'gameroom__scores-display');
|
||||
this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay);
|
||||
this.timerDisplay = this.createElement('div', 'gameroom__timer-display');
|
||||
this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay, this.timerDisplay);
|
||||
|
||||
// Affichage du mot caché
|
||||
this.wordDisplay = this.createElement('div', 'gameroom__word-display');
|
||||
@@ -195,7 +197,9 @@ export class GameRoomWindow extends Window {
|
||||
currentPlayerIndex: 0,
|
||||
guessedLetters: [],
|
||||
scores: {},
|
||||
counter: 0
|
||||
counter: 0,
|
||||
counterRound: 0,
|
||||
timer: 0
|
||||
};
|
||||
|
||||
this.initDrawing();
|
||||
@@ -375,10 +379,11 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
this.socket.on('game-player-left', (data) => {
|
||||
this.showMessage(`${data.username} a quitté le salon`, 'info');
|
||||
console.log(`${data.username} left the room`);
|
||||
|
||||
if (this.gameState.isPlaying)
|
||||
{
|
||||
if (this.gameState.players)
|
||||
if (Array.isArray(this.gameState.players))
|
||||
this.gameState.players = this.gameState.players.filter(p => p !== data.username);
|
||||
}
|
||||
|
||||
@@ -494,7 +499,7 @@ export class GameRoomWindow extends Window {
|
||||
// If spectating, return to spectator list
|
||||
if (this.isSpectating) {
|
||||
this.resetGameUI();
|
||||
this.currentRoom = null;
|
||||
// this.currentRoom = null;
|
||||
this.isSpectating = false;
|
||||
this.switchTab('spectator');
|
||||
this.showMessage('La partie est terminée', 'info');
|
||||
@@ -519,6 +524,8 @@ export class GameRoomWindow extends Window {
|
||||
this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
|
||||
this.gameState.players = data.players;
|
||||
this.gameState.scores = data.scores || {};
|
||||
this.gameState.timer = data.timer || 0;
|
||||
this.updateTimerUI();
|
||||
|
||||
this.showGameUI();
|
||||
this.updateWordDisplay();
|
||||
@@ -608,6 +615,15 @@ export class GameRoomWindow extends Window {
|
||||
// Setup UI for new round with new drawer
|
||||
this.setupRound();
|
||||
});
|
||||
|
||||
this.socket.on('game-timer-sync', (data) => {
|
||||
this.gameState.timer = data.remaining;
|
||||
this.updateTimerUI();
|
||||
});
|
||||
|
||||
this.socket.on('game-timer-ended', (data) => {
|
||||
this.showMessage(data.message || 'Temps écoulé !', 'info');
|
||||
})
|
||||
}
|
||||
|
||||
disconnectGameSocket() {
|
||||
@@ -752,8 +768,7 @@ export class GameRoomWindow extends Window {
|
||||
return;
|
||||
}
|
||||
|
||||
this.roomsList = data || [];
|
||||
this.renderRoomsList(this.roomsList);
|
||||
this.renderRoomsList(data || []);
|
||||
} catch (error) {
|
||||
console.error('Load rooms error:', error);
|
||||
this.showMessage('Erreur de connexion', 'error');
|
||||
@@ -1102,7 +1117,9 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
|
||||
async loadLobby() {
|
||||
console.log('Loading lobby for room:', this.currentRoom);
|
||||
if (!this.currentRoom) return;
|
||||
console.log('Managed to load lobby, current room:', this.currentRoom);
|
||||
|
||||
this.gameState.scores = {};
|
||||
|
||||
@@ -1241,6 +1258,12 @@ export class GameRoomWindow extends Window {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
updateTimerUI()
|
||||
{
|
||||
if (this.timerDisplay)
|
||||
this.timerDisplay.textContent = `Temps restant : ${this.gameState.timer}s`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOGIQUE DU JEU
|
||||
// ============================================
|
||||
@@ -1295,6 +1318,9 @@ export class GameRoomWindow extends Window {
|
||||
this.isSpectating = false;
|
||||
|
||||
this.gameState.scores = {};
|
||||
this.gameState.counter = 0;
|
||||
this.gameState.counterRound = 0;
|
||||
this.gameState.timer = 0;
|
||||
this.gameState.players = [];
|
||||
this.gameState.currentPlayerIndex = 0;
|
||||
this.gameState.guessedLetters = [];
|
||||
@@ -1518,7 +1544,7 @@ export class GameRoomWindow extends Window {
|
||||
const pointsText = points !== 0 ? ` (${points > 0 ? '+' : ''}${points} pts)` : '';
|
||||
|
||||
if (success) {
|
||||
item.textContent = `${username}: "${guess}" - Bonne ${typeText}!${pointsText}`;
|
||||
item.textContent = `${username}: "${guess}" - Bon ${typeText}!${pointsText}`;
|
||||
} else {
|
||||
item.textContent = `${username}: "${guess}" - Mauvais ${typeText}${pointsText}`;
|
||||
}
|
||||
@@ -1568,7 +1594,10 @@ export class GameRoomWindow extends Window {
|
||||
this.wordDisplay.textContent = word.split('').join(' ');
|
||||
|
||||
// Auto next round after delay
|
||||
this.gameState.counterRound++;
|
||||
setTimeout(() => {
|
||||
if (this.gameState.counterRound >= (this.gameState.players.length * 4))
|
||||
this.endGame();
|
||||
if (this.gameState.isPlaying) {
|
||||
this.nextRound();
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ export class LoginWindow extends Window {
|
||||
bindEvents() {
|
||||
this.loginBtn.addEventListener('click', () => this.handleLogin());
|
||||
this.registerBtn.addEventListener('click', () => this.handleRegister());
|
||||
this.githubBtn.addEventListener('click', () => this.handleGitHubLogin());
|
||||
|
||||
this.githubBtn.addEventListener('click', () => {console.log(API.AUTH.GITHUB); this.handleGitHubLogin();});
|
||||
|
||||
// Login with Enter
|
||||
this.passwordInput.addEventListener('keypress', (e) => {
|
||||
@@ -194,6 +195,8 @@ export class LoginWindow extends Window {
|
||||
const left = (screen.width - width) / 2;
|
||||
const top = (screen.height - height) / 2;
|
||||
|
||||
console.log(API.AUTH.GITHUB);
|
||||
|
||||
const popup = window.open(
|
||||
API.AUTH.GITHUB,
|
||||
'githubOAuth',
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { colorizeText, updateElement } from "./tools.js";
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
export class Popup {
|
||||
|
||||
constructor(msg, parent = document.body) {
|
||||
this.msg = msg;
|
||||
this.parent = parent;
|
||||
this.obj = updateElement({
|
||||
parent: parent,
|
||||
classList: ['popup'],
|
||||
additionalStyles: {
|
||||
opacity: '0'
|
||||
}
|
||||
});
|
||||
|
||||
this.run();
|
||||
}
|
||||
|
||||
async create() {
|
||||
this.parent.appendChild(this.obj);
|
||||
|
||||
this.obj.style.transition = "opacity 0.5s ease";
|
||||
requestAnimationFrame(() => {
|
||||
this.obj.style.opacity = "1";
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
async write(speed = 30) {
|
||||
for (let i = 0; i < this.msg.length; i++) {
|
||||
this.obj.textContent += this.msg[i];
|
||||
// colorizeText();
|
||||
await new Promise(r => setTimeout(r, speed));
|
||||
}
|
||||
}
|
||||
async remove() {
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
this.obj.style.transition = "opacity 0.3s ease";
|
||||
this.obj.style.opacity = "0";
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
if (this.obj.parentNode) {
|
||||
this.obj.parentNode.removeChild(this.obj);
|
||||
}
|
||||
}
|
||||
async run() {
|
||||
await this.create();
|
||||
await this.write();
|
||||
await this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* /////////////////////////////////////////// */
|
||||
// render in color the text of all .multicolor
|
||||
export function colorizeText() {
|
||||
|
||||
const elements = document.querySelectorAll(".multicolor");
|
||||
|
||||
const colorizeText = (el) => {
|
||||
const text = el.textContent;
|
||||
el.innerHTML = "";
|
||||
|
||||
const baseHue = Math.random() * 360;
|
||||
|
||||
// 🎲 random step = makes rainbow "scrambled"
|
||||
const step = (Math.random() * 60) + 10; // 10 → 70
|
||||
|
||||
// 🎲 random direction (left or right rainbow)
|
||||
const direction = Math.random() < 0.5 ? 1 : -1;
|
||||
|
||||
[...text].forEach((char, i) => {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = char;
|
||||
|
||||
const hue = baseHue + (i * step * direction);
|
||||
|
||||
span.style.color = `hsl(${hue}, 90%, 60%)`;
|
||||
|
||||
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
|
||||
|
||||
el.appendChild(span);
|
||||
});
|
||||
};
|
||||
elements.forEach(colorizeText);
|
||||
}
|
||||
|
||||
export function updateElement({
|
||||
el, // existing element or null to create new
|
||||
parent = document.body,
|
||||
id = null,
|
||||
classList = [], // object like { css - classes to add }
|
||||
textContent = "",
|
||||
additionalStyles = {} // object like { color: 'red', display: 'flex' }
|
||||
} = {}) {
|
||||
// If no element passed, create a div by default
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
parent.appendChild(el);
|
||||
}
|
||||
|
||||
// Set ID if provided
|
||||
if (id) el.id = id;
|
||||
|
||||
// Manage classes
|
||||
classList.forEach(cls => el.classList.add(cls));
|
||||
|
||||
// Set text content
|
||||
if (textContent !== undefined) el.textContent = textContent;
|
||||
|
||||
Object.assign(el.style, additionalStyles);
|
||||
|
||||
return el;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #1f001f, #1f1f00);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
color: white;
|
||||
animation: float 2s ease-in-out infinite alternate;
|
||||
border: 5px double #654050;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
.box:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
|
||||
animation: bobble 0.4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bobble {
|
||||
from {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
to {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
background: #142d4a;
|
||||
height: 100px;
|
||||
|
||||
margin: 15px;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 25px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "Roboto";
|
||||
font-size: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@property --deg {
|
||||
syntax: '<angle>';
|
||||
inherits: true;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
.box::before,
|
||||
.box::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: conic-gradient(
|
||||
from var(--deg) at center,
|
||||
#00c3ff,
|
||||
#4d0199,
|
||||
#6300c6,
|
||||
#009dcd
|
||||
);
|
||||
border-radius: inherit;
|
||||
z-index: -2;
|
||||
padding: 2px;
|
||||
animation: autoRotate 2s linear infinite;
|
||||
}
|
||||
.box::after {
|
||||
filter: blur(10px);
|
||||
}
|
||||
@keyframes autoRotate {
|
||||
to{ --deg: 360deg; }
|
||||
}
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
.popup {
|
||||
max-width: 300px;
|
||||
background-color: #41030c;
|
||||
margin: 30px;
|
||||
margin-right: -50px;
|
||||
padding: 1em;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
border: 10px solid var(--clr-accent);
|
||||
border-radius: 50px;
|
||||
position: fixed;
|
||||
right: 50%;
|
||||
top: 50%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Wiskas</title>
|
||||
<link rel="stylesheet" href="wiskas.css" />
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="helloText"></h1>
|
||||
|
||||
<nav class="game" aria-label="Game">
|
||||
<button class="box multicolor" data-action="Home page" aria-label="Home Page"
|
||||
onclick="window.location.href='../index.html'">Home Page</button>
|
||||
</nav>
|
||||
|
||||
<nav class="login" aria-label="Game">
|
||||
<button class="box multicolor" aria-label="Home Page" id="login-button">Login</button>
|
||||
</nav>
|
||||
|
||||
<div id="login"></div>
|
||||
|
||||
<script type="module" src="./wiskas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Popup } from "./popup.js";
|
||||
import { colorizeText, updateElement } from "./tools.js";
|
||||
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
|
||||
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
// 2️⃣ Add click handler
|
||||
async function tryLogin() {
|
||||
const login = prompt("Enter your 42 login:"); // Ask for a login
|
||||
|
||||
if (!login) return;
|
||||
|
||||
try {
|
||||
// Call your backend route
|
||||
const res = await fetch(`/api/intra/profile/${login}`);
|
||||
|
||||
if (!res.ok) {
|
||||
new Popup('Please, who do you think we are?\nWe already know all about you.\nNow enter your correct login and nobody gets hurt');
|
||||
const errorData = await res.json();
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log("Profile data:", data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error("Fetch failed:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
export class Wiskas {
|
||||
constructor(parent = document.body) {
|
||||
this.parent = parent;
|
||||
this.obj = updateElement({
|
||||
parent: parent,
|
||||
classList: ['wiskas']
|
||||
})
|
||||
|
||||
this.json_login = '';
|
||||
this.index_chaberu = 0;
|
||||
this.iniChat();
|
||||
}
|
||||
|
||||
chaberu() {
|
||||
let num = Math.min(this.index_chaberu, this.discussions.length - 1);
|
||||
let text = this.discussions[num];
|
||||
new Popup(text, this.obj);
|
||||
this.index_chaberu++;
|
||||
}
|
||||
|
||||
iniChat() {
|
||||
this.discussions = ['Well hi there...',
|
||||
'Please refrain from touching\n the yellow button without\n beeing logged in',
|
||||
'We are going to take actions\n if you continue..',
|
||||
'Actions already taken\n you are only making it worse'];
|
||||
}
|
||||
|
||||
async login() {
|
||||
let answer = await tryLogin();
|
||||
if (!answer) return;
|
||||
this.json_login = answer;
|
||||
|
||||
let dataUser = {
|
||||
firstName: this.json_login.usual_first_name ?? this.json_login.first_name,
|
||||
lastName: this.json_login.last_name,
|
||||
photo: this.json_login.image.link,
|
||||
month: this.json_login.pool_month,
|
||||
year: this.json_login.pool_year,
|
||||
projects: this.json_login.projects_users.filter(project => project.status === "in_progress").map(project => project.project.name),
|
||||
perfect: this.json_login.projects_users.filter(project => project.final_mark === 125).map(project => project.project.name),
|
||||
|
||||
};
|
||||
this.discussions = [
|
||||
`Welcome ${dataUser.firstName} ${dataUser.lastName}.`,
|
||||
`We heard quite a lot about the piscine of ${dataUser.month} ${dataUser.year}...\nIt's suprising to see you here`,
|
||||
`How is your ${dataUser.projects[Math.floor(Math.random() * dataUser.projects.length)]} coming along?`,
|
||||
`Perfect score for ${dataUser.perfect[Math.floor(Math.random() * dataUser.perfect.length)]}, impressive.. Should you really spend so much time in front of a screen?`,
|
||||
`Shouldn't you be working on your ${dataUser.projects[Math.floor(Math.random() * dataUser.projects.length)]}?`,
|
||||
`Quite an ugly human...\n but then again, you arent a cat`
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
let el = document.getElementById('helloText');
|
||||
let img = document.createElement('img');
|
||||
img.src = '../assets/wiskas-the-third.jpg';
|
||||
el.append(img);
|
||||
/* /////////////////////////////////////////// */
|
||||
colorizeText();
|
||||
/* /////////////////////////////////////////// */
|
||||
// 1️⃣ Create a button dynamically
|
||||
const app = document.getElementById('login');
|
||||
|
||||
|
||||
let cat = new Wiskas;
|
||||
|
||||
let buttonLogin = document.getElementById('login-button');
|
||||
Object.assign(buttonLogin.style, {
|
||||
position: 'fixed', // make sure it's fixed
|
||||
top: '0',
|
||||
left: '0',
|
||||
right: 'auto' // remove the right: 0 if it comes from CSS
|
||||
});
|
||||
buttonLogin.addEventListener('click', () => cat.login());
|
||||
img.addEventListener('click', () => { cat.chaberu() })
|
||||
|
||||