Compare commits
102 Commits
44c88c8f60
...
4.18.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5af5785fa0 | ||
|
|
9510147806 | ||
| e17f59e614 | |||
| 8126b46a0a | |||
| 25dd9264b5 | |||
| 5f9ef2beb0 | |||
| ccf993bff2 | |||
| 1d05bd43b4 | |||
| 3e067346ff | |||
| b2527ed041 | |||
|
|
06d150b8c5 | ||
| 0cbac61b4a | |||
| 088dda8d52 | |||
|
|
c3d10f31ac | ||
| e4384ee894 | |||
|
|
d667a2f68c | ||
| 35f608513d | |||
|
|
13038b0cb9 | ||
| b17aae5795 | |||
|
|
421f769cb6 | ||
| bd7a055a09 | |||
|
|
e2a2b56174 | ||
| 2a29ca8cdd | |||
|
|
266cbe7509 | ||
| e8b31b1748 | |||
|
|
8812b0fad4 | ||
| dd5e8e65e5 | |||
|
|
bf6ffeadb0 | ||
| fa3d21e303 | |||
|
|
33efc4e107 | ||
| 8d697fd311 | |||
|
|
b9e60b5d4a | ||
| 270f44cc1f | |||
|
|
73dbe5826a | ||
| 194df5691c | |||
|
|
49a1bd40ff | ||
| f847424b9c | |||
|
|
d50a576e31 | ||
|
|
eba2ad6232 | ||
| 14961cce01 | |||
|
|
dcb5d7373f | ||
| d57e6efa98 | |||
|
|
4156e1c9ce | ||
| 358556612e | |||
|
|
7f82d2eeae | ||
| a55f0b4b61 | |||
|
|
f115c03ecb | ||
| fd2467a9ea | |||
|
|
9d3f3940a9 | ||
| 0541bb58d1 | |||
|
|
89a6aa22f7 | ||
| 6e17328846 | |||
| 0037820905 | |||
| cfcd967ce0 | |||
| 1f96290371 | |||
| 2aee79bb68 | |||
| 46c96d4ceb | |||
|
|
14b4473f72 | ||
| 1ef5e8a72f | |||
|
|
576e5af87e | ||
| 3c0828fdbe | |||
|
|
ae7f04abc3 | ||
| b81bb3d0ae | |||
| 52e5033afc | |||
|
|
10a26404b3 | ||
|
|
11478a096d | ||
| 8ca909db52 | |||
|
|
1edb3bfd89 | ||
| 9738a04b7a | |||
|
|
2bc50664e0 | ||
| 3e3a062a06 | |||
| 641c892981 | |||
| a58b2e03b1 | |||
| e60fe7c98d | |||
| 370de175db | |||
| 5d245d0011 | |||
| c220e54bb8 | |||
|
|
b847d3c054 | ||
| c7dd72ecc2 | |||
| 42a5adbae0 | |||
|
|
ae9a8f2af9 | ||
| 7adc8b8645 | |||
|
|
146348470f | ||
| 5e503cbc36 | |||
|
|
126e2030ae | ||
| 54e3215127 | |||
|
|
72d2845772 | ||
| 64a7a63ab3 | |||
|
|
51c36348b9 | ||
| 2e54880302 | |||
|
|
266406fe7c | ||
| 5c6d3ac436 | |||
|
|
674619dadc | ||
|
|
ef539d3eea | ||
|
|
e09463b138 | ||
| 4d6ea54771 | |||
| aa83082d09 | |||
| 42e7597e26 | |||
|
|
6befd9c722 | ||
| 51d9c0b5f6 | |||
| de565b52dc | |||
| 051e7406e3 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -134,7 +134,9 @@ target
|
|||||||
/.project
|
/.project
|
||||||
/.settings
|
/.settings
|
||||||
/RUNNING_PID
|
/RUNNING_PID
|
||||||
|
/knockoutwhistwebfrontend/
|
||||||
/knockoutwhist/
|
/knockoutwhist/
|
||||||
/knockoutwhistweb/.g8/
|
/knockoutwhistweb/.g8/
|
||||||
/knockoutwhistweb/.bsp/
|
/knockoutwhistweb/.bsp/
|
||||||
/currentSnapshot.json
|
/currentSnapshot.json
|
||||||
|
.env
|
||||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -2,3 +2,8 @@
|
|||||||
path = knockoutwhist
|
path = knockoutwhist
|
||||||
branch = main
|
branch = main
|
||||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
||||||
|
|
||||||
|
[submodule "knockoutwhistfrontend"]
|
||||||
|
path = knockoutwhistfrontend
|
||||||
|
branch = main
|
||||||
|
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Frontend.git
|
||||||
|
|||||||
2
.sbtopts
Normal file
2
.sbtopts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-J--add-opens=java.base/java.util=ALL-UNNAMED
|
||||||
|
-J--add-opens=java.base/java.lang=ALL-UNNAMED
|
||||||
284
CHANGELOG.md
284
CHANGELOG.md
@@ -1,4 +1,4 @@
|
|||||||
## (2025-11-03)
|
## (2025-11-07)
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
@@ -7,7 +7,10 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||||
|
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
|
||||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||||
|
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
|
||||||
|
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
|
||||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||||
@@ -18,17 +21,22 @@
|
|||||||
|
|
||||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||||
|
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
|
||||||
|
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
|
||||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||||
|
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
|
||||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||||
|
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
|
||||||
|
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
|
||||||
|
|
||||||
### Reverts
|
### Reverts
|
||||||
|
|
||||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||||
## (2025-11-03)
|
## (2025-11-07)
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
@@ -37,7 +45,10 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||||
|
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
|
||||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||||
|
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
|
||||||
|
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
|
||||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||||
@@ -48,94 +59,221 @@
|
|||||||
|
|
||||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||||
|
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
|
||||||
|
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
|
||||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||||
|
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
|
||||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||||
|
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
|
||||||
|
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
|
||||||
|
|
||||||
### Reverts
|
### Reverts
|
||||||
|
|
||||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||||
## (2025-11-03)
|
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
## (2025-11-07)
|
||||||
|
|
||||||
* implemented multigame support (#34)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
|
||||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
|
||||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
|
||||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
|
||||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
|
||||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
|
||||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
* ensure proper CMD syntax in Dockerfile ([#48](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/48)) ([5c6d3ac](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5c6d3ac436f6d23a36f58b6835c9bd50feddc789))
|
||||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
## (2025-11-07)
|
||||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
|
||||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
|
||||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
|
||||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
|
||||||
|
|
||||||
### Reverts
|
|
||||||
|
|
||||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
|
||||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
|
||||||
## (2025-11-03)
|
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
|
||||||
|
|
||||||
* implemented multigame support (#34)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
|
||||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
|
||||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
|
||||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
|
||||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
|
||||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
|
||||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
* changelog syntax ([2e54880](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e548803020c99f62644283fcf3570048261173a))
|
||||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
## (2025-11-07)
|
||||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
|
||||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
|
||||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
|
||||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
|
||||||
|
|
||||||
### Reverts
|
|
||||||
|
|
||||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
|
||||||
## (2025-11-03)
|
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
|
||||||
|
|
||||||
* implemented multigame support (#34)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
|
||||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
|
||||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
|
||||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
|
||||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
|
||||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
|
||||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
* removed trailing ([64a7a63](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/64a7a63ab3dff59e66f62328e3b5865bb177fcde))
|
||||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
## (2025-11-07)
|
||||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
|
||||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
### Bug Fixes
|
||||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
|
||||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
* traling ([54e3215](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/54e321512777f6722864694eb677eab0e8418a9f))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removed trailing ([5e503cb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5e503cbc364f7cb23926976acc6cee575eadd9d6))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* trailing ([7adc8b8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7adc8b8645390cd18d63b4eee6db8ef448b7a46a))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removed trailing ([c7dd72e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c7dd72ecc2786ef63daf2b4288093025a8e22bfd))
|
||||||
|
* removed trailing ([42a5adb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/42a5adbae01802587a48a9de15bad44b5ef014cf))
|
||||||
|
## (2025-11-20)
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **game:** Fixed polling, SPA, Gameplayloop etc. (#59)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ci:** Polling ([370de17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/370de175db65edf87e7ab211190977b540a39d85))
|
||||||
|
* **ci:** Polling Added polling for when the game starts and a card gets played ([#58](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/58)) ([e60fe7c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e60fe7c98dcab05949140a8a54ed6e4e2fbbc022))
|
||||||
|
* **game:** Fixed polling, SPA, Gameplayloop etc. ([#59](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/59)) ([a58b2e0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a58b2e03b11a54667d63ba6604f579a8e328c9d1))
|
||||||
|
* **ui:** added js routing, updated ingame ui, added tricktable ([#50](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/50)) ([c220e54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c220e54bb8d87f4f0f37a089bcd993e8df806123)), closes [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43) [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43)
|
||||||
|
* **ui:** implement tie & trump menu, fixed some critical bugs ([#52](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/52)) ([5d245d0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5d245d0011a5fb03193514303b45702cd8329224))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **polling:** Improve polling mechanism and delay handling ([#60](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/60)) ([641c892](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/641c892981649eb85640527cc0fe325ff683fa77))
|
||||||
|
## (2025-11-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** Fixed a bug where the game would reload on game start ([#81](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/81)) ([9738a04](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/9738a04b7a3c63c8cd1450e563ec04823fb3c35a))
|
||||||
|
## (2025-11-23)
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **websocket:** Implement WebSocket connection and event handling (#82)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **websocket:** Implement WebSocket connection and event handling ([#82](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/82)) ([8ca909d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8ca909db522dd7108a3e40ce84811eaf8695eaa5))
|
||||||
|
## (2025-11-24)
|
||||||
|
## (2025-11-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** Implement received hand event handling and UI updates ([#83](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/83)) ([52e5033](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/52e5033afca344ae40a644196555a9655913710a)), closes [#76](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/76)
|
||||||
|
* **base:** Fixed logic for websockets and added GameStateEvent. Might've caused instability on other feature branches! ([#84](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/84)) ([b81bb3d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b81bb3d0aeb8500a9d7417a10e24e7f8a17d71d2))
|
||||||
|
## (2025-11-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** Implemented card played event via websocket ([#85](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/85)) ([3c0828f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3c0828fdbeb507706b86f1662476c46e760533e4))
|
||||||
|
## (2025-11-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** Implemented session closed and kick event via websocket ([#87](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/87)) ([1ef5e8a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1ef5e8a72fdf8a3d1ae624c8c3d7c6595017bc6f))
|
||||||
|
## (2025-12-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** Implemented turn event via websocket ([#86](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/86)) ([2aee79b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2aee79bb6887008397aa0780d1d74ce96af1c202))
|
||||||
|
* GameState to Title Mapping BAC-1 ([#92](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/92)) ([6e17328](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6e17328846745375482c97383b143d86a86e7f32))
|
||||||
|
* **ui:** Implement countless feature using the SJWP ([#89](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/89)) ([1f96290](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f962903712163543fd4f98e696be5e7e29d88a6))
|
||||||
|
* **ui:** Popups ([#91](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/91)) ([0037820](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/003782090509bca1c5022c308231b7560dd9b23d))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** Fixed websocket routing ([#88](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/88)) ([46c96d4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/46c96d4ceb935ac91fc515a1fdaef195e5ebc0a7))
|
||||||
|
* **api:** fixes - reimplemented animations ([#90](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/90)) ([cfcd967](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/cfcd967ce08ecf07f3f06826c337f684eb3b0c5f))
|
||||||
|
## (2025-12-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** BAC-10 Websockets - Kick Users ([#93](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/93)) ([0541bb5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/0541bb58d19efd98d134b3d0412f39b4b1001783))
|
||||||
|
## (2025-12-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** BAC-11 Websocket - Return to Lobby ([#94](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/94)) ([fd2467a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fd2467a9ea22dca64d5152a5a3e6db86d9a6f345))
|
||||||
|
## (2025-12-01)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** BAC-23 Remove old polling code ([#95](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/95)) ([a55f0b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a55f0b4b6164a47e3524422650ed99d10f9c8b0d))
|
||||||
|
## (2025-12-01)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* FRO-6 Websocket Close Handle ([#96](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/96)) ([3585566](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/358556612ec74601c8b31125e4e65f750abf8c4c))
|
||||||
|
## (2025-12-03)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ui:** FRO-7 Endscreen ([#97](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/97)) ([d57e6ef](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d57e6efa985ca07c32f9f54595fe7393dbdf4d8a))
|
||||||
|
## (2025-12-03)
|
||||||
|
## (2025-12-03)
|
||||||
|
## (2025-12-04)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* BAC-25 Race Condition: Websocket Promises ([#99](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/99)) ([f847424](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f847424b9cea423ace5661d1efb6e4f01483c655))
|
||||||
|
## (2025-12-04)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* FRO-3 FRO-4 Added vue compontents to ingame and lobby ([#100](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/100)) ([194df56](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/194df5691ccda1c21ebe9157c4396a4a21aa921d))
|
||||||
|
## (2025-12-05)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* BAC-29 Implement Mappers for Common Classes ([#101](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/101)) ([270f44c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/270f44cc1f3447ffcc33fb19a47c52391c69972b))
|
||||||
|
## (2025-12-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* BAC-30 Implement Jackson Mapping via DTOs ([#102](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/102)) ([8d697fd](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8d697fd311478cf792b4631377de4522ecbda9f7))
|
||||||
|
## (2025-12-10)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* FRO-29 Websocket Communication ([#104](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/104)) ([fa3d21e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fa3d21e3038eb07369764850a9ad9badd269ac57))
|
||||||
|
## (2025-12-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* BAC-27 Implemented endpoint which returns information about the current state ([#103](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/103)) ([dd5e8e6](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/dd5e8e65e55f02a7618b3c60e8fc7087774e5106))
|
||||||
|
## (2025-12-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* FRO-2 Implement Login Component ([#105](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/105)) ([e8b31b1](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e8b31b174819b5f033034501856c4b1189c4c4ee))
|
||||||
|
## (2025-12-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* FRO-20 Create scoreboard component ([#106](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/106)) ([2a29ca8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2a29ca8cdd3ef55f6f66f00b5e7727e1b1af1458))
|
||||||
|
## (2025-12-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** FRO-14 Create Game ([#107](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/107)) ([bd7a055](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bd7a055a0944a1c5219f21bb080bf658229f49e9))
|
||||||
|
## (2025-12-11)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* FRO-31 Small backend changes ([#108](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/108)) ([b17aae5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b17aae5795b35ce3805db87c9bf741a5a96cd5ac))
|
||||||
|
## (2025-12-14)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Update routing and websocket configuration for game state management ([#109](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/109)) ([35f6085](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/35f608513dd80eece46d49b40ecf31c8e915d307))
|
||||||
|
## (2025-12-18)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ui:** FRO-36 PWA ([#110](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/110)) ([e4384ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e4384ee8945ac462fe1f3580215117e0a438f71a))
|
||||||
|
## (2026-01-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Enhance win effects and animations in OfflineView component ([088dda8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/088dda8d528edb9f3fd420e7e69eb44144d39eff))
|
||||||
|
## (2026-01-07)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add GatewayModule for enhanced module configuration ([1d05bd4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1d05bd43b482564636345322a2cecf58f7d229d0))
|
||||||
|
* Enhance Dockerfile with secret management for GitHub credentials ([5f9ef2b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5f9ef2beb0a6e7d5e8caf436f545bbd78a6b242e))
|
||||||
|
* Simplify Dockerfile by removing multi-stage build and adjusting file copy paths ([e17f59e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e17f59e614a0060b70514c2337ac6fd84688546e))
|
||||||
|
* Update build.sbt and Dockerfile for improved GitHub credentials handling ([8126b46](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8126b46a0a1649d2b20b31975603f7610fafd18b))
|
||||||
|
* Update build.sbt and Dockerfile for improved GitHub credentials handling ([25dd926](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/25dd9264b57ffafc5b01587ff5dda2e2188b5fbd))
|
||||||
|
* Update Dockerfile for multi-platform support and add nginx configuration ([ccf993b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf993bff25b4551a70c1f0263695f828df15a02))
|
||||||
|
* Update Dockerfile for multi-platform support and add nginx configuration ([3e06734](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3e067346ffe3bdc62dc936ea8e79ae9293d86351))
|
||||||
|
* Update Dockerfile for multi-platform support and add nginx configuration ([b2527ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b2527ed041568d20f880515b406fe0b0e10c12c1))
|
||||||
|
## (2026-01-07)
|
||||||
|
|||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM --platform=$TARGETPLATFORM eclipse-temurin:22-jre-alpine
|
||||||
|
|
||||||
|
# Install Argon2 CLI and libraries
|
||||||
|
RUN apk add --no-cache bash argon2 argon2-libs
|
||||||
|
|
||||||
|
WORKDIR /opt/playapp
|
||||||
|
|
||||||
|
# Copy staged Play build
|
||||||
|
COPY ./knockoutwhistweb/target/universal/stage /opt/playapp
|
||||||
|
|
||||||
|
# Expose the default Play port
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PLAY_HTTP_PORT=9000
|
||||||
|
|
||||||
|
# Run the Play app
|
||||||
|
ENTRYPOINT ["./bin/knockoutwhistweb"]
|
||||||
|
CMD ["-Dplay.server.pidfile.path=/dev/null"]
|
||||||
30
build.sbt
30
build.sbt
@@ -1,12 +1,19 @@
|
|||||||
ThisBuild / scalaVersion := "3.5.1"
|
ThisBuild / scalaVersion := "3.5.1"
|
||||||
|
|
||||||
|
credentials += Credentials(
|
||||||
|
"GitHub Package Registry",
|
||||||
|
"maven.pkg.github.com",
|
||||||
|
sys.env.getOrElse("GITHUB_USER", sys.error("GITHUB_USER not set")),
|
||||||
|
sys.env.getOrElse("GITHUB_TOKEN", sys.error("GITHUB_TOKEN not set"))
|
||||||
|
)
|
||||||
|
|
||||||
lazy val commonSettings = Seq(
|
lazy val commonSettings = Seq(
|
||||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.18",
|
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.19",
|
||||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % "test",
|
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test",
|
||||||
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.0.1",
|
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.1.0",
|
||||||
libraryDependencies += "org.scalafx" %% "scalafx" % "22.0.0-R33",
|
libraryDependencies += "org.scalafx" %% "scalafx" % "24.0.2-R36",
|
||||||
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.3.0",
|
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.4.0",
|
||||||
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M1",
|
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M9",
|
||||||
libraryDependencies ++= {
|
libraryDependencies ++= {
|
||||||
// Determine OS version of JavaFX binaries
|
// Determine OS version of JavaFX binaries
|
||||||
lazy val osName = System.getProperty("os.name") match {
|
lazy val osName = System.getProperty("os.name") match {
|
||||||
@@ -19,7 +26,6 @@ lazy val commonSettings = Seq(
|
|||||||
.map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName)
|
.map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName)
|
||||||
},
|
},
|
||||||
libraryDependencies += guice,
|
libraryDependencies += guice,
|
||||||
coverageEnabled := true,
|
|
||||||
coverageFailOnMinimum := true,
|
coverageFailOnMinimum := true,
|
||||||
coverageMinimumStmtTotal := 85,
|
coverageMinimumStmtTotal := 85,
|
||||||
coverageMinimumBranchTotal := 100
|
coverageMinimumBranchTotal := 100
|
||||||
@@ -36,11 +42,17 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
|||||||
.enablePlugins(PlayScala)
|
.enablePlugins(PlayScala)
|
||||||
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
||||||
.settings(
|
.settings(
|
||||||
|
|
||||||
|
resolvers += "GitHub Packages" at "https://maven.pkg.github.com/16Janis12/KnockOutWhist-Web",
|
||||||
|
|
||||||
commonSettings,
|
commonSettings,
|
||||||
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
|
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
|
||||||
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
||||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0",
|
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
||||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2"
|
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
||||||
|
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
|
||||||
|
libraryDependencies += "de.janis" % "knockoutwhist-data" % "1.0-SNAPSHOT",
|
||||||
|
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
lazy val root = (project in file("."))
|
||||||
|
|||||||
Submodule knockoutwhist updated: b9a7b0a2af...10fa4badf0
1
knockoutwhistfrontend
Submodule
1
knockoutwhistfrontend
Submodule
Submodule knockoutwhistfrontend added at e972f1c432
@@ -1,6 +1,14 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background-image: url('/assets/images/background.png');
|
--background-image: url('/assets/images/background.png') !important;
|
||||||
--color: white;
|
--color: #f8f9fa !important; /* Light text on dark bg */
|
||||||
|
--highlightscolor: rgba(131, 131, 131, 0.75) !important;
|
||||||
|
--background-color: #192734;
|
||||||
|
/* Bootstrap variable overrides for dark mode */
|
||||||
|
--bs-body-color: var(--color);
|
||||||
|
--bs-link-color: #66b2ff;
|
||||||
|
--bs-link-hover-color: #99ccff;
|
||||||
|
--bs-border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
--bs-heading-color: var(--color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--background-image: url('/assets/images/img.png');
|
--background-image: url('/assets/images/img.png');
|
||||||
--color: black;
|
--color: black;
|
||||||
|
--highlightscolor: rgba(0, 0, 0, 0.75);
|
||||||
|
--background-color: rgba(228, 232, 237, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3; /* ensure card sits above the particles */
|
z-index: 3; /* ensure card sits above the particles */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,94 @@
|
|||||||
@import "dark-mode.less";
|
@import "dark-mode.less";
|
||||||
@import "login.less";
|
@import "login.less";
|
||||||
|
|
||||||
|
/* Provide default (light) variables so the site works even if light-mode.less fails */
|
||||||
|
:root {
|
||||||
|
--background-image: url('/assets/images/img.png');
|
||||||
|
--color: #212529; /* Bootstrap body text default */
|
||||||
|
|
||||||
|
/* Bootstrap variable overrides for light mode */
|
||||||
|
--bs-body-color: var(--color) !important;
|
||||||
|
--bs-link-color: #0d6efd !important;
|
||||||
|
--bs-link-hover-color: #0a58ca !important;
|
||||||
|
--bs-border-color: rgba(0, 0, 0, 0.125) !important;
|
||||||
|
--bs-heading-color: var(--color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@background-color: var(--background-color);
|
||||||
|
@highlightcolor: var(--highlightscolor);
|
||||||
@background-image: var(--background-image);
|
@background-image: var(--background-image);
|
||||||
@color: var(--color);
|
@color: var(--color);
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
0% { transform: translateX(-100vw); }
|
0% {
|
||||||
100% { transform: translateX(0); }
|
transform: translateX(-100vw);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-field-background {
|
.game-field-background {
|
||||||
background-image: @background-image;
|
background-image: @background-image;
|
||||||
background-size: 100vw 100vh;
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby-background {
|
||||||
|
background-color: @background-color;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggle {
|
||||||
|
float: none;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handcard :hover {
|
||||||
|
box-shadow: 3px 3px 3px @highlightcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* cover the whole container */
|
||||||
|
background: rgba(0, 0, 0, 0.50);
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 6px;
|
||||||
|
pointer-events: none; /* user can't click through overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-div {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 1400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure body text color follows theme variable and works with Bootstrap */
|
||||||
|
body {
|
||||||
|
color: @color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: @color;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
flex-grow: 1; /* fill remaining vertical space as visual footer background */
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-field {
|
.game-field {
|
||||||
@@ -19,17 +97,28 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-drop-shadow {
|
||||||
|
box-shadow: 0 1px 15px 0 #000000
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingame-side-shadow {
|
||||||
|
box-shadow: 0 1px 15px 0 #000000
|
||||||
|
}
|
||||||
|
|
||||||
#sessions {
|
#sessions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
animation: slideIn 0.5s ease-out forwards;
|
animation: slideIn 0.5s ease-out forwards;
|
||||||
animation-fill-mode: backwards;
|
animation-fill-mode: backwards;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#textanimation {
|
#textanimation {
|
||||||
animation: slideIn 0.5s ease-out forwards;
|
animation: slideIn 0.5s ease-out forwards;
|
||||||
animation-fill-mode: backwards;
|
animation-fill-mode: backwards;
|
||||||
@@ -41,6 +130,7 @@
|
|||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
font-family: Arial, serif;
|
font-family: Arial, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ingame {
|
#ingame {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -48,45 +138,59 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ingame a, #ingame h1, #ingame p {
|
#ingame a, #ingame h1, #ingame p {
|
||||||
color: @color;
|
color: @color;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
font-family: Arial, serif;
|
font-family: Arial, serif;
|
||||||
}
|
}
|
||||||
#playercards {
|
|
||||||
display: flex;
|
.ingame-cards-slide {
|
||||||
flex-direction: row;
|
div {
|
||||||
justify-content: center;
|
|
||||||
height: 20%;
|
|
||||||
img {
|
|
||||||
animation: slideIn 0.5s ease-out forwards;
|
animation: slideIn 0.5s ease-out forwards;
|
||||||
animation-fill-mode: backwards;
|
animation-fill-mode: backwards;
|
||||||
&:nth-child(1) { animation-delay: 0.5s; }
|
|
||||||
&:nth-child(2) { animation-delay: 1s; }
|
&:nth-child(1) {
|
||||||
&:nth-child(3) { animation-delay: 1.5s; }
|
animation-delay: 0.5s;
|
||||||
&:nth-child(4) { animation-delay: 2s; }
|
}
|
||||||
&:nth-child(5) { animation-delay: 2.5s; }
|
|
||||||
&:nth-child(6) { animation-delay: 3s; }
|
&:nth-child(2) {
|
||||||
&:nth-child(7) { animation-delay: 3.5s; }
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 2.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(7) {
|
||||||
|
animation-delay: 3.5s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#cardsplayed {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 10%;
|
|
||||||
min-height: 10%
|
|
||||||
}
|
|
||||||
#playedcardplayer {
|
#playedcardplayer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playedcardplayer p {
|
#playedcardplayer p {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 4%;
|
height: 4%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playedcardplayer img {
|
#playedcardplayer img {
|
||||||
height: 90%;
|
height: 90%;
|
||||||
}
|
}
|
||||||
@@ -98,45 +202,95 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
#firstCardObject {
|
#firstCardObject {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-right: 4%;
|
margin-right: 4%;
|
||||||
}
|
}
|
||||||
#firstCardObject img{
|
|
||||||
|
#firstCardObject img {
|
||||||
height: 90%;
|
height: 90%;
|
||||||
}
|
}
|
||||||
#firstCardObject p{
|
|
||||||
|
#firstCardObject p {
|
||||||
height: 10%;
|
height: 10%;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
}
|
}
|
||||||
#trumpsuit {
|
|
||||||
display: flex;
|
#next-players-container {
|
||||||
flex-direction: row;
|
|
||||||
margin-left: 4%;
|
|
||||||
}
|
|
||||||
#nextPlayers {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
#selecttrumpsuit {
|
#selecttrumpsuit {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rules {
|
#rules {
|
||||||
color: @color;
|
color: @color;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
font-family: Arial, serif;
|
font-family: Arial, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-table {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In-game centered stage and blurred sides overlay */
|
||||||
|
.ingame-stage {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper that adds a backdrop blur to the background outside the centered card */
|
||||||
|
.blur-sides {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create an overlay that blurs everything behind it, except the central content area */
|
||||||
|
.blur-sides::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* fallback: subtle vignette if backdrop-filter unsupported */
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0.35) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
|
||||||
|
.blur-sides::before {
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
-webkit-backdrop-filter: blur(10px) saturate(110%);
|
||||||
|
backdrop-filter: blur(10px) saturate(110%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,13 +15,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
|
|||||||
|
|
||||||
override def executionContext: ExecutionContext = ec
|
override def executionContext: ExecutionContext = ec
|
||||||
|
|
||||||
private def getUserFromSession(request: RequestHeader): Option[User] = {
|
|
||||||
val session = request.cookies.get("sessionId")
|
|
||||||
if (session.isDefined)
|
|
||||||
return sessionManager.getUserBySession(session.get.value)
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
override def invokeBlock[A](
|
override def invokeBlock[A](
|
||||||
request: Request[A],
|
request: Request[A],
|
||||||
block: AuthenticatedRequest[A] => Future[Result]
|
block: AuthenticatedRequest[A] => Future[Result]
|
||||||
@@ -30,8 +23,15 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
|
|||||||
case Some(user) =>
|
case Some(user) =>
|
||||||
block(new AuthenticatedRequest(user, request))
|
block(new AuthenticatedRequest(user, request))
|
||||||
case None =>
|
case None =>
|
||||||
Future.successful(Results.Redirect(routes.UserController.login()))
|
Future.successful(Results.Unauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||||
|
val session = request.cookies.get("accessToken")
|
||||||
|
if (session.isDefined)
|
||||||
|
return sessionManager.getUserBySession(session.get.value)
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,11 +2,13 @@ package components
|
|||||||
|
|
||||||
import de.knockoutwhist.components.DefaultConfiguration
|
import de.knockoutwhist.components.DefaultConfiguration
|
||||||
import de.knockoutwhist.ui.UI
|
import de.knockoutwhist.ui.UI
|
||||||
|
import de.knockoutwhist.utils.DelayHandler
|
||||||
import de.knockoutwhist.utils.events.EventListener
|
import de.knockoutwhist.utils.events.EventListener
|
||||||
|
|
||||||
class WebApplicationConfiguration extends DefaultConfiguration {
|
class WebApplicationConfiguration extends DefaultConfiguration {
|
||||||
|
|
||||||
override def uis: Set[UI] = Set()
|
override def uis: Set[UI] = Set()
|
||||||
override def listener: Set[EventListener] = Set()
|
|
||||||
|
override def listener: Set[EventListener] = Set(DelayHandler)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,50 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import auth.{AuthAction, AuthenticatedRequest}
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
import de.knockoutwhist.control.GameState
|
||||||
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
import de.knockoutwhist.control.GameState.*
|
||||||
|
import exceptions.*
|
||||||
import logic.PodManager
|
import logic.PodManager
|
||||||
import model.sessions.{PlayerSession, UserSession}
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import model.users.User
|
||||||
import play.api.*
|
import play.api.*
|
||||||
|
import play.api.libs.json.{JsValue, Json}
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
|
import play.twirl.api.Html
|
||||||
|
import util.GameUtil
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.*
|
import javax.inject.*
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller creates an `Action` to handle HTTP requests to the
|
|
||||||
* application's home page.
|
|
||||||
*/
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class IngameController @Inject()(
|
class IngameController @Inject()(
|
||||||
val controllerComponents: ControllerComponents,
|
val cc: ControllerComponents,
|
||||||
val authAction: AuthAction,
|
val authAction: AuthAction,
|
||||||
val podManager: PodManager
|
implicit val ec: ExecutionContext
|
||||||
) extends BaseController {
|
) extends AbstractController(cc) {
|
||||||
|
|
||||||
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
g.logic.getCurrentState match {
|
val results = Try {
|
||||||
case Lobby => Ok("Lobby: " + gameId)
|
IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user)
|
||||||
case InGame =>
|
}
|
||||||
Ok(views.html.ingame.ingame(
|
if (results.isSuccess) {
|
||||||
g.getPlayerByUser(request.user),
|
Ok(views.html.main("Knockout Whist - " + GameUtil.stateToTitle(g.logic.getCurrentState))(results.get))
|
||||||
g.logic
|
} else {
|
||||||
))
|
InternalServerError(results.failed.get.getMessage)
|
||||||
case SelectTrump =>
|
|
||||||
Ok(views.html.ingame.selecttrump(
|
|
||||||
g.getPlayerByUser(request.user),
|
|
||||||
g.logic
|
|
||||||
))
|
|
||||||
case TieBreak =>
|
|
||||||
Ok(views.html.ingame.tie(
|
|
||||||
g.getPlayerByUser(request.user),
|
|
||||||
g.logic
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
|
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
NotFound("Game not found")
|
Redirect(routes.MainMenuController.mainMenu())
|
||||||
}
|
}
|
||||||
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
val result = Try {
|
val result = Try {
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
@@ -63,52 +54,84 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
NoContent
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url,
|
||||||
|
"content" -> IngameController.returnInnerHTML(game.get, game.get.logic.getCurrentState, request.user).toString()
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
throwable match {
|
||||||
case _: NotInThisGameException =>
|
case _: NotInThisGameException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotHostException =>
|
case _: NotHostException =>
|
||||||
Forbidden(throwable.getMessage)
|
Forbidden(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotEnoughPlayersException =>
|
case _: NotEnoughPlayersException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(throwable.getMessage)
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = podManager.getGame(gameId)
|
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = PodManager.getGame(gameId)
|
||||||
|
val playerToKickUUID = UUID.fromString(playerToKick)
|
||||||
val result = Try {
|
val result = Try {
|
||||||
game match {
|
game.get.leaveGame(playerToKickUUID, true)
|
||||||
case Some(g) =>
|
|
||||||
g.addUser(request.user)
|
|
||||||
case None =>
|
|
||||||
NotFound("Game not found")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
Redirect(routes.IngameController.game(gameId))
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
val throwable = result.failed.get
|
InternalServerError(Json.obj(
|
||||||
throwable match {
|
"status" -> "failure",
|
||||||
case _: GameFullException =>
|
"errorMessage" -> "Something went wrong."
|
||||||
BadRequest(throwable.getMessage)
|
))
|
||||||
case _: IllegalArgumentException =>
|
|
||||||
BadRequest(throwable.getMessage)
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(throwable.getMessage)
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(throwable.getMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = PodManager.getGame(gameId)
|
||||||
|
val result = Try {
|
||||||
|
game.get.leaveGame(request.user.id, false)
|
||||||
}
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
|
||||||
|
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Something went wrong."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
val jsonBody = request.body.asJson
|
||||||
|
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "cardID").asOpt[String]
|
||||||
|
}
|
||||||
cardIdOpt match {
|
cardIdOpt match {
|
||||||
case Some(cardId) =>
|
case Some(cardId) =>
|
||||||
var optSession: Option[UserSession] = None
|
var optSession: Option[UserSession] = None
|
||||||
@@ -120,35 +143,67 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
optSession.foreach(_.lock.unlock())
|
optSession.foreach(_.lock.unlock())
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
NoContent
|
Ok(Json.obj(
|
||||||
|
"status" -> "success"
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
throwable match {
|
||||||
case _: CantPlayCardException =>
|
case _: CantPlayCardException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotInThisGameException =>
|
case _: NotInThisGameException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalArgumentException =>
|
case _: IllegalArgumentException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalStateException =>
|
case _: IllegalStateException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _: NotInteractableException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(throwable.getMessage)
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
BadRequest("cardId parameter is missing")
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "cardId Parameter is missing"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
NotFound("Game not found")
|
NotFound(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Game not found"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) => {
|
case Some(g) => {
|
||||||
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
val jsonBody = request.body.asJson
|
||||||
|
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "cardID").asOpt[String]
|
||||||
|
}
|
||||||
var optSession: Option[UserSession] = None
|
var optSession: Option[UserSession] = None
|
||||||
val result = Try {
|
val result = Try {
|
||||||
cardIdOpt match {
|
cardIdOpt match {
|
||||||
@@ -173,15 +228,30 @@ class IngameController @Inject()(
|
|||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
throwable match {
|
||||||
case _: CantPlayCardException =>
|
case _: CantPlayCardException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotInThisGameException =>
|
case _: NotInThisGameException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalArgumentException =>
|
case _: IllegalArgumentException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalStateException =>
|
case _: IllegalStateException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(throwable.getMessage)
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,11 +260,15 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
|
val jsonBody = request.body.asJson
|
||||||
|
val trumpOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "trump").asOpt[String]
|
||||||
|
}
|
||||||
trumpOpt match {
|
trumpOpt match {
|
||||||
case Some(trump) =>
|
case Some(trump) =>
|
||||||
var optSession: Option[UserSession] = None
|
var optSession: Option[UserSession] = None
|
||||||
@@ -211,13 +285,25 @@ class IngameController @Inject()(
|
|||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
throwable match {
|
||||||
case _: IllegalArgumentException =>
|
case _: IllegalArgumentException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotInThisGameException =>
|
case _: NotInThisGameException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalStateException =>
|
case _: IllegalStateException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(throwable.getMessage)
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
@@ -227,11 +313,15 @@ class IngameController @Inject()(
|
|||||||
NotFound("Game not found")
|
NotFound("Game not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = PodManager.getGame(gameId)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
|
val jsonBody = request.body.asJson
|
||||||
|
val tieOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "tie").asOpt[String]
|
||||||
|
}
|
||||||
tieOpt match {
|
tieOpt match {
|
||||||
case Some(tie) =>
|
case Some(tie) =>
|
||||||
var optSession: Option[UserSession] = None
|
var optSession: Option[UserSession] = None
|
||||||
@@ -248,13 +338,25 @@ class IngameController @Inject()(
|
|||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
throwable match {
|
||||||
case _: IllegalArgumentException =>
|
case _: IllegalArgumentException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: NotInThisGameException =>
|
case _: NotInThisGameException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _: IllegalStateException =>
|
case _: IllegalStateException =>
|
||||||
BadRequest(throwable.getMessage)
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(throwable.getMessage)
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
@@ -265,4 +367,78 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = PodManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
val result = Try {
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
g.returnToLobby(session)
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: NotInThisGameException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _: IllegalStateException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _ =>
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
NotFound(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Game not found"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object IngameController {
|
||||||
|
|
||||||
|
def returnInnerHTML(gameLobby: GameLobby, gameState: GameState, user: User): Html = {
|
||||||
|
gameState match {
|
||||||
|
case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
|
||||||
|
case InGame =>
|
||||||
|
views.html.ingame.ingame(
|
||||||
|
gameLobby.getPlayerByUser(user),
|
||||||
|
gameLobby
|
||||||
|
)
|
||||||
|
case SelectTrump =>
|
||||||
|
views.html.ingame.selecttrump(
|
||||||
|
gameLobby.getPlayerByUser(user),
|
||||||
|
gameLobby
|
||||||
|
)
|
||||||
|
case TieBreak =>
|
||||||
|
views.html.ingame.tie(
|
||||||
|
gameLobby.getPlayerByUser(user),
|
||||||
|
gameLobby
|
||||||
|
)
|
||||||
|
case FinishedMatch =>
|
||||||
|
views.html.ingame.finishedMatch(
|
||||||
|
Some(user),
|
||||||
|
gameLobby
|
||||||
|
)
|
||||||
|
case _ =>
|
||||||
|
throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.AuthAction
|
||||||
|
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
|
||||||
|
import play.api.routing.JavaScriptReverseRouter
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class JavaScriptRoutingController @Inject()(
|
||||||
|
val controllerComponents: ControllerComponents,
|
||||||
|
val authAction: AuthAction,
|
||||||
|
) extends BaseController {
|
||||||
|
def javascriptRoutes(): Action[AnyContent] =
|
||||||
|
Action { implicit request =>
|
||||||
|
Ok(
|
||||||
|
JavaScriptReverseRouter("jsRoutes")(
|
||||||
|
routes.javascript.MainMenuController.createGame,
|
||||||
|
routes.javascript.MainMenuController.joinGame,
|
||||||
|
routes.javascript.MainMenuController.navSPA,
|
||||||
|
routes.javascript.UserController.login_Post
|
||||||
|
)
|
||||||
|
).as("text/javascript")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package controllers
|
|||||||
import auth.{AuthAction, AuthenticatedRequest}
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
import logic.PodManager
|
import logic.PodManager
|
||||||
import play.api.*
|
import play.api.*
|
||||||
|
import play.api.libs.json.Json
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
|
|
||||||
import javax.inject.*
|
import javax.inject.*
|
||||||
@@ -15,13 +16,12 @@ import javax.inject.*
|
|||||||
@Singleton
|
@Singleton
|
||||||
class MainMenuController @Inject()(
|
class MainMenuController @Inject()(
|
||||||
val controllerComponents: ControllerComponents,
|
val controllerComponents: ControllerComponents,
|
||||||
val authAction: AuthAction,
|
val authAction: AuthAction
|
||||||
val podManager: PodManager
|
|
||||||
) extends BaseController {
|
) extends BaseController {
|
||||||
|
|
||||||
// Pass the request-handling function directly to authAction (no nested Action)
|
// Pass the request-handling function directly to authAction (no nested Action)
|
||||||
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
Ok(views.html.mainmenu.navbar(Some(request.user)))
|
Ok(views.html.main("Knockout Whist - Create Game")(views.html.mainmenu.creategame(Some(request.user))))
|
||||||
}
|
}
|
||||||
|
|
||||||
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
@@ -29,33 +29,85 @@ class MainMenuController @Inject()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val gameLobby = podManager.createGame(
|
val jsonBody = request.body.asJson
|
||||||
|
if (jsonBody.isDefined) {
|
||||||
|
val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String]
|
||||||
|
.getOrElse(s"${request.user.name}'s Game")
|
||||||
|
|
||||||
|
val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String]
|
||||||
|
.getOrElse(throw new IllegalArgumentException("Player amount is required."))
|
||||||
|
|
||||||
|
val gameLobby = PodManager.createGame(
|
||||||
host = request.user,
|
host = request.user,
|
||||||
name = s"${request.user.name}'s Game",
|
name = gamename,
|
||||||
maxPlayers = 4
|
maxPlayers = playeramount.toInt
|
||||||
)
|
)
|
||||||
Redirect(routes.IngameController.game(gameLobby.id))
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"gameId" -> gameLobby.id,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Invalid form submission"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val postData = request.body.asFormUrlEncoded
|
val jsonBody = request.body.asJson
|
||||||
if (postData.isDefined) {
|
val gameId: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("")
|
(jsValue \ "gameId").asOpt[String]
|
||||||
val game = podManager.getGame(gameId)
|
}
|
||||||
|
if (gameId.isDefined) {
|
||||||
|
val game = PodManager.getGame(gameId.get)
|
||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
Redirect(routes.IngameController.joinGame(gameId))
|
g.addUser(request.user)
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(g.id).url,
|
||||||
|
"content" -> IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user).toString
|
||||||
|
))
|
||||||
case None =>
|
case None =>
|
||||||
NotFound("Game not found")
|
NotFound(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "No Game found"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BadRequest("Invalid form submission")
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Invalid form submission"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def rules(): Action[AnyContent] = {
|
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
Action { implicit request =>
|
Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
|
||||||
Ok(views.html.mainmenu.rules())
|
}
|
||||||
|
|
||||||
|
def navSPA(location: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
location match {
|
||||||
|
case "0" => // Main Menu
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
|
||||||
|
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
|
||||||
|
))
|
||||||
|
case "1" => // Rules
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.MainMenuController.rules().url,
|
||||||
|
"content" -> views.html.mainmenu.rules(Some(request.user)).toString
|
||||||
|
))
|
||||||
|
case _ =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Invalid form submission"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
94
knockoutwhistweb/app/controllers/StatusController.scala
Normal file
94
knockoutwhistweb/app/controllers/StatusController.scala
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.AuthAction
|
||||||
|
import logic.PodManager
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import logic.user.SessionManager
|
||||||
|
import model.users.User
|
||||||
|
import play.api.libs.json.{JsValue, Json}
|
||||||
|
import play.api.mvc.*
|
||||||
|
import util.WebsocketEventMapper
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class StatusController @Inject()(
|
||||||
|
val controllerComponents: ControllerComponents,
|
||||||
|
val sessionManager: SessionManager,
|
||||||
|
val authAction: AuthAction
|
||||||
|
) extends BaseController {
|
||||||
|
|
||||||
|
def requestStatus(): Action[AnyContent] = {
|
||||||
|
Action { implicit request =>
|
||||||
|
val userOpt = getUserFromSession(request)
|
||||||
|
if (userOpt.isEmpty) {
|
||||||
|
Ok(
|
||||||
|
Json.obj(
|
||||||
|
"status" -> "unauthenticated"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val user = userOpt.get
|
||||||
|
val gameOpt = PodManager.identifyGameOfUser(user)
|
||||||
|
if (gameOpt.isEmpty) {
|
||||||
|
Ok(
|
||||||
|
Json.obj(
|
||||||
|
"status" -> "authenticated",
|
||||||
|
"username" -> user.name,
|
||||||
|
"inGame" -> false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val game = gameOpt.get
|
||||||
|
Ok(
|
||||||
|
Json.obj(
|
||||||
|
"status" -> "authenticated",
|
||||||
|
"username" -> user.name,
|
||||||
|
"inGame" -> true,
|
||||||
|
"gameId" -> game.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def game(gameId: String): Action[AnyContent] = {
|
||||||
|
Action { implicit request =>
|
||||||
|
val userOpt = getUserFromSession(request)
|
||||||
|
if (userOpt.isEmpty) {
|
||||||
|
Unauthorized("User not authenticated")
|
||||||
|
} else {
|
||||||
|
val user = userOpt.get
|
||||||
|
val gameOpt = PodManager.getGame(gameId)
|
||||||
|
if (gameOpt.isEmpty) {
|
||||||
|
NotFound("Game not found")
|
||||||
|
} else {
|
||||||
|
val game = gameOpt.get
|
||||||
|
if (!game.getPlayers.contains(user.id)) {
|
||||||
|
Forbidden("User not part of this game")
|
||||||
|
} else {
|
||||||
|
Ok(
|
||||||
|
Json.obj(
|
||||||
|
"gameId" -> game.id,
|
||||||
|
"state" -> game.logic.getCurrentState.toString,
|
||||||
|
"data" -> mapGameState(game, user)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
private def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||||
|
val session = request.cookies.get("accessToken")
|
||||||
|
if (session.isDefined)
|
||||||
|
return sessionManager.getUserBySession(session.get.value)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
private def mapGameState(gameLobby: GameLobby, user: User): JsValue = {
|
||||||
|
val userSession = gameLobby.getUserSession(user.id)
|
||||||
|
WebsocketEventMapper.stateToJson(userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import auth.{AuthAction, AuthenticatedRequest}
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import dto.subDTO.UserDTO
|
||||||
import logic.user.{SessionManager, UserManager}
|
import logic.user.{SessionManager, UserManager}
|
||||||
|
import model.users.User
|
||||||
import play.api.*
|
import play.api.*
|
||||||
|
import play.api.libs.json.Json
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
|
import play.api.mvc.Cookie.SameSite.{Lax, None, Strict}
|
||||||
|
|
||||||
import javax.inject.*
|
import javax.inject.*
|
||||||
|
|
||||||
@@ -20,36 +24,32 @@ class UserController @Inject()(
|
|||||||
val authAction: AuthAction
|
val authAction: AuthAction
|
||||||
) extends BaseController {
|
) extends BaseController {
|
||||||
|
|
||||||
def login(): Action[AnyContent] = {
|
|
||||||
Action { implicit request =>
|
|
||||||
val session = request.cookies.get("sessionId")
|
|
||||||
if (session.isDefined) {
|
|
||||||
val possibleUser = sessionManager.getUserBySession(session.get.value)
|
|
||||||
if (possibleUser.isDefined) {
|
|
||||||
Redirect(routes.MainMenuController.mainMenu())
|
|
||||||
} else {
|
|
||||||
Ok(views.html.login.login())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(views.html.login.login())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def login_Post(): Action[AnyContent] = {
|
def login_Post(): Action[AnyContent] = {
|
||||||
Action { implicit request =>
|
Action { implicit request =>
|
||||||
val postData = request.body.asFormUrlEncoded
|
val jsonBody = request.body.asJson
|
||||||
if (postData.isDefined) {
|
val username: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "username").asOpt[String]
|
||||||
|
}
|
||||||
|
val password: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "password").asOpt[String]
|
||||||
|
}
|
||||||
|
if (username.isDefined && password.isDefined) {
|
||||||
// Extract username and password from form data
|
// Extract username and password from form data
|
||||||
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")
|
val possibleUser = userManager.authenticate(username.get, password.get)
|
||||||
val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
|
|
||||||
val possibleUser = userManager.authenticate(username, password)
|
|
||||||
if (possibleUser.isDefined) {
|
if (possibleUser.isDefined) {
|
||||||
Redirect(routes.MainMenuController.mainMenu()).withCookies(
|
Ok(Json.obj(
|
||||||
Cookie("sessionId", sessionManager.createSession(possibleUser.get))
|
"user" -> Json.obj(
|
||||||
|
"id" -> possibleUser.get.id,
|
||||||
|
"username" -> possibleUser.get.name
|
||||||
)
|
)
|
||||||
|
)).withCookies(Cookie(
|
||||||
|
name = "accessToken",
|
||||||
|
value = sessionManager.createSession(possibleUser.get),
|
||||||
|
httpOnly = true,
|
||||||
|
secure = false,
|
||||||
|
sameSite = Some(Lax)
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
println("Failed login attempt for user: " + username)
|
|
||||||
Unauthorized("Invalid username or password")
|
Unauthorized("Invalid username or password")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -58,13 +58,20 @@ class UserController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the request-handling function directly to authAction (no nested Action)
|
def getUserInfo(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
def logout(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
val user: User = request.user
|
||||||
val sessionCookie = request.cookies.get("sessionId")
|
Ok(Json.obj(
|
||||||
|
"id" -> user.id,
|
||||||
|
"username" -> user.name
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
def logoutPost(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val sessionCookie = request.cookies.get("accessToken")
|
||||||
if (sessionCookie.isDefined) {
|
if (sessionCookie.isDefined) {
|
||||||
sessionManager.invalidateSession(sessionCookie.get.value)
|
sessionManager.invalidateSession(sessionCookie.get.value)
|
||||||
}
|
}
|
||||||
Redirect(routes.UserController.login()).discardingCookies(DiscardingCookie("sessionId"))
|
NoContent.discardingCookies(DiscardingCookie("accessToken"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
45
knockoutwhistweb/app/controllers/WebsocketController.scala
Normal file
45
knockoutwhistweb/app/controllers/WebsocketController.scala
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
|
||||||
|
import auth.AuthAction
|
||||||
|
import logic.PodManager
|
||||||
|
import logic.user.SessionManager
|
||||||
|
import model.sessions.{UserSession, UserWebsocketActor}
|
||||||
|
import org.apache.pekko.actor.{ActorRef, ActorSystem, Props}
|
||||||
|
import org.apache.pekko.stream.Materializer
|
||||||
|
import play.api.*
|
||||||
|
import play.api.libs.streams.ActorFlow
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import javax.inject.*
|
||||||
|
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class WebsocketController @Inject()(
|
||||||
|
cc: ControllerComponents,
|
||||||
|
val sessionManger: SessionManager,
|
||||||
|
)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
|
||||||
|
|
||||||
|
def socket(): WebSocket = WebSocket.accept[String, String] { request =>
|
||||||
|
val session = request.cookies.get("accessToken")
|
||||||
|
if (session.isEmpty) throw new Exception("No session cookie found")
|
||||||
|
val userOpt = sessionManger.getUserBySession(session.get.value)
|
||||||
|
if (userOpt.isEmpty) throw new Exception("Invalid session")
|
||||||
|
val user = userOpt.get
|
||||||
|
val game = PodManager.identifyGameOfUser(user)
|
||||||
|
if (game.isEmpty) throw new Exception("User is not in a game")
|
||||||
|
val userSession = game.get.getUserSession(user.id)
|
||||||
|
ActorFlow.actorRef { out =>
|
||||||
|
println("Connect received")
|
||||||
|
KnockOutWebSocketActorFactory.create(out, userSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object KnockOutWebSocketActorFactory {
|
||||||
|
def create(out: ActorRef, userSession: UserSession): Props = {
|
||||||
|
Props(new UserWebsocketActor(out, userSession))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
35
knockoutwhistweb/app/dto/GameInfoDTO.scala
Normal file
35
knockoutwhistweb/app/dto/GameInfoDTO.scala
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import dto.subDTO.*
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
case class GameInfoDTO(
|
||||||
|
gameId: String,
|
||||||
|
self: Option[PlayerDTO],
|
||||||
|
hand: Option[HandDTO],
|
||||||
|
playerQueue: PlayerQueueDTO,
|
||||||
|
currentTrick: Option[TrickDTO],
|
||||||
|
currentRound: Option[RoundDTO]
|
||||||
|
)
|
||||||
|
|
||||||
|
object GameInfoDTO {
|
||||||
|
|
||||||
|
def apply(lobby: GameLobby, user: User): GameInfoDTO = {
|
||||||
|
val selfPlayer = Try {
|
||||||
|
Some(lobby.getPlayerByUser(user))
|
||||||
|
}.getOrElse(None)
|
||||||
|
|
||||||
|
GameInfoDTO(
|
||||||
|
gameId = lobby.id,
|
||||||
|
self = selfPlayer.map(PlayerDTO(_)),
|
||||||
|
hand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_)),
|
||||||
|
playerQueue = PlayerQueueDTO(lobby.logic),
|
||||||
|
currentTrick = lobby.logic.getCurrentTrick.map(TrickDTO(_)),
|
||||||
|
currentRound = lobby.logic.getCurrentRound.map(r => RoundDTO(r, lobby.logic.getCurrentMatch))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
knockoutwhistweb/app/dto/LobbyInfoDTO.scala
Normal file
22
knockoutwhistweb/app/dto/LobbyInfoDTO.scala
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import dto.subDTO.UserDTO
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
case class LobbyInfoDTO(gameId: String, users: List[UserDTO], self: UserDTO, maxPlayers: Int)
|
||||||
|
|
||||||
|
object LobbyInfoDTO {
|
||||||
|
|
||||||
|
def apply(lobby: GameLobby, user: User): LobbyInfoDTO = {
|
||||||
|
val session = lobby.getUserSession(user.id)
|
||||||
|
|
||||||
|
LobbyInfoDTO(
|
||||||
|
gameId = lobby.id,
|
||||||
|
users = lobby.getPlayers.values.map(user => UserDTO(user)).toList,
|
||||||
|
self = UserDTO(session),
|
||||||
|
maxPlayers = lobby.maxPlayers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
knockoutwhistweb/app/dto/TieInfoDTO.scala
Normal file
27
knockoutwhistweb/app/dto/TieInfoDTO.scala
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import dto.subDTO.PlayerDTO
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
case class TieInfoDTO(gameId: String, currentPlayer: Option[PlayerDTO], self: Option[PlayerDTO], tiedPlayers: Seq[PlayerDTO], highestAmount: Int)
|
||||||
|
|
||||||
|
object TieInfoDTO {
|
||||||
|
|
||||||
|
def apply(lobby: GameLobby, user: User): TieInfoDTO = {
|
||||||
|
val selfPlayer = Try {
|
||||||
|
Some(lobby.getPlayerByUser(user))
|
||||||
|
}.getOrElse(None)
|
||||||
|
|
||||||
|
TieInfoDTO(
|
||||||
|
gameId = lobby.id,
|
||||||
|
currentPlayer = lobby.logic.playerTieLogic.currentTiePlayer().map(PlayerDTO.apply),
|
||||||
|
self = selfPlayer.map(PlayerDTO.apply),
|
||||||
|
tiedPlayers = lobby.logic.playerTieLogic.getTiedPlayers.map(PlayerDTO.apply),
|
||||||
|
highestAmount = lobby.logic.playerTieLogic.highestAllowedNumber()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
knockoutwhistweb/app/dto/TrumpInfoDTO.scala
Normal file
31
knockoutwhistweb/app/dto/TrumpInfoDTO.scala
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import dto.subDTO.{HandDTO, PlayerDTO}
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
case class TrumpInfoDTO(
|
||||||
|
gameId: String,
|
||||||
|
chooser: Option[PlayerDTO],
|
||||||
|
self: Option[PlayerDTO],
|
||||||
|
selfHand: Option[HandDTO],
|
||||||
|
)
|
||||||
|
|
||||||
|
object TrumpInfoDTO {
|
||||||
|
|
||||||
|
def apply(lobby: GameLobby, user: User): TrumpInfoDTO = {
|
||||||
|
val selfPlayer = Try {
|
||||||
|
Some(lobby.getPlayerByUser(user))
|
||||||
|
}.getOrElse(None)
|
||||||
|
|
||||||
|
TrumpInfoDTO(
|
||||||
|
gameId = lobby.id,
|
||||||
|
chooser = lobby.logic.getTrumpPlayer.map(PlayerDTO(_)),
|
||||||
|
self = selfPlayer.map(PlayerDTO(_)),
|
||||||
|
selfHand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
knockoutwhistweb/app/dto/WonInfoDTO.scala
Normal file
34
knockoutwhistweb/app/dto/WonInfoDTO.scala
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import dto.subDTO.PodiumPlayerDTO
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
case class WonInfoDTO(
|
||||||
|
gameId: String,
|
||||||
|
winner: Option[PodiumPlayerDTO],
|
||||||
|
allPlayers: Seq[PodiumPlayerDTO]
|
||||||
|
)
|
||||||
|
|
||||||
|
object WonInfoDTO {
|
||||||
|
|
||||||
|
def apply(lobby: GameLobby, user: User): WonInfoDTO = {
|
||||||
|
val matchImpl = lobby.logic.getCurrentMatch
|
||||||
|
if (matchImpl.isEmpty) {
|
||||||
|
throw new IllegalStateException("No current match available in game logic")
|
||||||
|
}
|
||||||
|
val allPlayersDTO: Seq[PodiumPlayerDTO] = matchImpl.get.totalplayers.map { player =>
|
||||||
|
PodiumPlayerDTO(lobby.logic, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selfPlayerDTO = lobby.getPlayerByUser(user)
|
||||||
|
val winnerDTO = lobby.logic.getWinner
|
||||||
|
|
||||||
|
WonInfoDTO(
|
||||||
|
gameId = lobby.id,
|
||||||
|
winner = winnerDTO.map(player => PodiumPlayerDTO(lobby.logic, player)),
|
||||||
|
allPlayers = allPlayersDTO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
knockoutwhistweb/app/dto/subDTO/CardDTO.scala
Normal file
31
knockoutwhistweb/app/dto/subDTO/CardDTO.scala
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.cards.Card
|
||||||
|
import util.WebUIUtils
|
||||||
|
|
||||||
|
case class CardDTO(identifier: String, path: String, idx: Option[Int]) {
|
||||||
|
|
||||||
|
def toCard: Card = {
|
||||||
|
WebUIUtils.stringToCard(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object CardDTO {
|
||||||
|
|
||||||
|
def apply(card: Card, index: Int): CardDTO = {
|
||||||
|
CardDTO(
|
||||||
|
identifier = WebUIUtils.cardtoString(card),
|
||||||
|
path = WebUIUtils.cardToPath(card),
|
||||||
|
idx = Some(index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply(card: Card): CardDTO = {
|
||||||
|
CardDTO(
|
||||||
|
identifier = WebUIUtils.cardtoString(card),
|
||||||
|
path = WebUIUtils.cardToPath(card),
|
||||||
|
idx = None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
knockoutwhistweb/app/dto/subDTO/HandDTO.scala
Normal file
15
knockoutwhistweb/app/dto/subDTO/HandDTO.scala
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.cards.Hand
|
||||||
|
|
||||||
|
case class HandDTO(cards: List[CardDTO])
|
||||||
|
|
||||||
|
object HandDTO {
|
||||||
|
|
||||||
|
def apply(hand: Hand): HandDTO = {
|
||||||
|
HandDTO(
|
||||||
|
cards = hand.cards.zipWithIndex.map { case (card, idx) => CardDTO(card, idx) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.player.AbstractPlayer
|
||||||
|
|
||||||
|
case class PlayerDTO(id: String, name: String, dogLife: Boolean)
|
||||||
|
|
||||||
|
object PlayerDTO {
|
||||||
|
|
||||||
|
def apply(player: AbstractPlayer): PlayerDTO = {
|
||||||
|
PlayerDTO(
|
||||||
|
id = player.id.toString,
|
||||||
|
name = player.name,
|
||||||
|
dogLife = player.isInDogLife
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala
Normal file
19
knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.control.GameLogic
|
||||||
|
|
||||||
|
case class PlayerQueueDTO(currentPlayer: Option[PlayerDTO], queue: Seq[PlayerDTO])
|
||||||
|
|
||||||
|
object PlayerQueueDTO {
|
||||||
|
|
||||||
|
def apply(logic: GameLogic): PlayerQueueDTO = {
|
||||||
|
val currentPlayerDTO = logic.getCurrentPlayer.map(PlayerDTO(_))
|
||||||
|
val queueDTO = logic.getPlayerQueue.map(_.duplicate().flatMap(player => Some(PlayerDTO(player))).toSeq)
|
||||||
|
if (queueDTO.isEmpty) {
|
||||||
|
PlayerQueueDTO(currentPlayerDTO, Seq.empty)
|
||||||
|
} else {
|
||||||
|
PlayerQueueDTO(currentPlayerDTO, queueDTO.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala
Normal file
47
knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.control.GameLogic
|
||||||
|
import de.knockoutwhist.player.AbstractPlayer
|
||||||
|
import de.knockoutwhist.rounds.Match
|
||||||
|
|
||||||
|
case class PodiumPlayerDTO(
|
||||||
|
player: PlayerDTO,
|
||||||
|
position: Int,
|
||||||
|
roundsWon: Int,
|
||||||
|
tricksWon: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
object PodiumPlayerDTO {
|
||||||
|
|
||||||
|
def apply(gameLogic: GameLogic, player: AbstractPlayer): PodiumPlayerDTO = {
|
||||||
|
val matchImplOpt = gameLogic.getCurrentMatch
|
||||||
|
if (matchImplOpt.isEmpty) {
|
||||||
|
throw new IllegalStateException("No current match available in game logic")
|
||||||
|
}
|
||||||
|
val matchImpl: Match = matchImplOpt.get
|
||||||
|
var roundsWon = 0
|
||||||
|
var tricksWon = 0
|
||||||
|
for (round <- matchImpl.roundlist) {
|
||||||
|
if (round.winner.contains(player)) {
|
||||||
|
roundsWon += 1
|
||||||
|
}
|
||||||
|
for (trick <- round.tricklist) {
|
||||||
|
if (trick.winner.contains(player)) {
|
||||||
|
tricksWon += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PodiumPlayerDTO(
|
||||||
|
player = PlayerDTO(player),
|
||||||
|
position = if (gameLogic.getWinner.contains(player)) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
},
|
||||||
|
roundsWon = roundsWon,
|
||||||
|
tricksWon = tricksWon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
knockoutwhistweb/app/dto/subDTO/RoundDTO.scala
Normal file
20
knockoutwhistweb/app/dto/subDTO/RoundDTO.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.cards.Card
|
||||||
|
import de.knockoutwhist.cards.CardValue.Ace
|
||||||
|
import de.knockoutwhist.rounds.{Match, Round}
|
||||||
|
|
||||||
|
case class RoundDTO(trumpSuit: CardDTO, playersIn: Seq[PlayerDTO], firstRound: Boolean, trickList: List[TrickDTO])
|
||||||
|
|
||||||
|
object RoundDTO {
|
||||||
|
|
||||||
|
def apply(round: Round, matchImpl: Option[Match]): RoundDTO = {
|
||||||
|
RoundDTO(
|
||||||
|
trumpSuit = CardDTO(Card(Ace, round.trumpSuit)),
|
||||||
|
playersIn = matchImpl.map(_.playersIn.map(PlayerDTO(_))).getOrElse(Seq.empty),
|
||||||
|
firstRound = round.firstRound,
|
||||||
|
trickList = round.tricklist.map(trick => TrickDTO(trick))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
knockoutwhistweb/app/dto/subDTO/TrickDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/TrickDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import de.knockoutwhist.rounds.Trick
|
||||||
|
|
||||||
|
case class TrickDTO(cards: Map[String, CardDTO], firstCard: Option[CardDTO], winner: Option[PlayerDTO])
|
||||||
|
|
||||||
|
object TrickDTO {
|
||||||
|
|
||||||
|
def apply(trick: Trick): TrickDTO = {
|
||||||
|
TrickDTO(
|
||||||
|
cards = trick.cards.map { case (card, player) => player.name -> CardDTO(card) },
|
||||||
|
firstCard = trick.firstCard.map(card => CardDTO(card)),
|
||||||
|
winner = trick.winner.map(player => PlayerDTO(player))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
knockoutwhistweb/app/dto/subDTO/UserDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/UserDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dto.subDTO
|
||||||
|
|
||||||
|
import model.sessions.UserSession
|
||||||
|
|
||||||
|
case class UserDTO(id: String, username: String, host: Boolean = false)
|
||||||
|
|
||||||
|
object UserDTO {
|
||||||
|
|
||||||
|
def apply(user: UserSession): UserDTO = {
|
||||||
|
UserDTO(
|
||||||
|
id = user.id.toString,
|
||||||
|
username = user.name,
|
||||||
|
host = user.host
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
knockoutwhistweb/app/events/KickEvent.scala
Normal file
9
knockoutwhistweb/app/events/KickEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
case class KickEvent(user: User) extends UserEvent(user) {
|
||||||
|
|
||||||
|
override def id: String = "KickEvent"
|
||||||
|
|
||||||
|
}
|
||||||
9
knockoutwhistweb/app/events/LeftEvent.scala
Normal file
9
knockoutwhistweb/app/events/LeftEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
case class LeftEvent(user: User) extends UserEvent(user) {
|
||||||
|
|
||||||
|
override def id: String = "LeftEvent"
|
||||||
|
|
||||||
|
}
|
||||||
9
knockoutwhistweb/app/events/LobbyUpdateEvent.scala
Normal file
9
knockoutwhistweb/app/events/LobbyUpdateEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
|
||||||
|
case class LobbyUpdateEvent() extends SimpleEvent {
|
||||||
|
|
||||||
|
override def id: String = "LobbyUpdateEvent"
|
||||||
|
|
||||||
|
}
|
||||||
12
knockoutwhistweb/app/events/UserEvent.scala
Normal file
12
knockoutwhistweb/app/events/UserEvent.scala
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
abstract class UserEvent(user: User) extends SimpleEvent {
|
||||||
|
|
||||||
|
def userId: UUID = user.id
|
||||||
|
|
||||||
|
}
|
||||||
45
knockoutwhistweb/app/logic/Gateway.scala
Normal file
45
knockoutwhistweb/app/logic/Gateway.scala
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import de.knockoutwhist.data.Pod
|
||||||
|
import de.knockoutwhist.data.redis.RedisManager
|
||||||
|
import org.apache.pekko.actor.ActorSystem
|
||||||
|
import org.redisson.config.Config
|
||||||
|
import play.api.inject.ApplicationLifecycle
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.*
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Gateway @Inject()(
|
||||||
|
lifecycle: ApplicationLifecycle,
|
||||||
|
actorSystem: ActorSystem
|
||||||
|
)(implicit ec: ExecutionContext) {
|
||||||
|
|
||||||
|
val redis: RedisManager = {
|
||||||
|
val config: Config = Config()
|
||||||
|
config.useSingleServer.setAddress("valkey://" + sys.env.getOrElse("VALKEY_HOST", "localhost") + ":" + sys.env.getOrElse("VALKEY_PORT", "6379"))
|
||||||
|
RedisManager(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.continuousSyncPod(() => {
|
||||||
|
createPod()
|
||||||
|
})
|
||||||
|
|
||||||
|
def syncPod(): Unit = {
|
||||||
|
redis.syncPod(createPod())
|
||||||
|
}
|
||||||
|
|
||||||
|
private def createPod(): Pod = {
|
||||||
|
Pod(
|
||||||
|
UUID.randomUUID().toString,
|
||||||
|
PodManager.podName,
|
||||||
|
PodManager.podIp,
|
||||||
|
9000,
|
||||||
|
PodManager.getAllGameIds().asJava,
|
||||||
|
PodManager.allBoundUsers().asJava
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,16 +11,18 @@ import util.GameUtil
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
||||||
@Singleton
|
object PodManager {
|
||||||
class PodManager {
|
|
||||||
|
|
||||||
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
||||||
val podIp: String = System.getenv("POD_IP")
|
val podIp: String = System.getenv("POD_IP")
|
||||||
val podName: String = System.getenv("POD_NAME")
|
val podName: String = System.getenv("POD_NAME")
|
||||||
|
|
||||||
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
||||||
|
private val userSession: mutable.Map[User, String] = mutable.Map()
|
||||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||||
|
|
||||||
|
private[logic] var redis: Option[Gateway] = None
|
||||||
|
|
||||||
def createGame(
|
def createGame(
|
||||||
host: User,
|
host: User,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -35,6 +37,8 @@ class PodManager {
|
|||||||
host = host
|
host = host
|
||||||
)
|
)
|
||||||
sessions += (gameLobby.id -> gameLobby)
|
sessions += (gameLobby.id -> gameLobby)
|
||||||
|
registerUserToGame(host, gameLobby.id)
|
||||||
|
redis.foreach(gateway => gateway.syncPod())
|
||||||
gameLobby
|
gameLobby
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +46,36 @@ class PodManager {
|
|||||||
sessions.get(gameId)
|
sessions.get(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[logic] def removeGame(gameId: String): Unit = {
|
def registerUserToGame(user: User, gameId: String): Boolean = {
|
||||||
sessions.remove(gameId)
|
if (sessions.contains(gameId)) {
|
||||||
|
userSession += (user -> gameId)
|
||||||
|
redis.foreach(gateway => gateway.syncPod())
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def unregisterUserFromGame(user: User): Unit = {
|
||||||
|
userSession.remove(user)
|
||||||
|
redis.foreach(gateway => gateway.redis.invalidateUser(user.id.toString))
|
||||||
|
}
|
||||||
|
|
||||||
|
def identifyGameOfUser(user: User): Option[GameLobby] = {
|
||||||
|
userSession.get(user) match {
|
||||||
|
case Some(gameId) => sessions.get(gameId)
|
||||||
|
case None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[logic] def removeGame(gameId: String): Unit = {
|
||||||
|
sessions.remove(gameId)
|
||||||
|
redis.foreach(gateway => gateway.redis.invalidateGame(gameId))
|
||||||
|
// Also remove all user sessions associated with this game
|
||||||
|
userSession.filterInPlace((_, v) => v != gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private[logic] def getAllGameIds(): List[String] = sessions.keys.toList
|
||||||
|
private[logic] def allBoundUsers(): List[String] = userSession.keys.map(_.id.toString).toList
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import de.knockoutwhist.player.Playertype.HUMAN
|
|||||||
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||||
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
||||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||||
|
import events.{KickEvent, LeftEvent, LobbyUpdateEvent, UserEvent}
|
||||||
import exceptions.*
|
import exceptions.*
|
||||||
|
import logic.PodManager
|
||||||
import model.sessions.{InteractionType, UserSession}
|
import model.sessions.{InteractionType, UserSession}
|
||||||
import model.users.User
|
import model.users.User
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.{Timer, TimerTask, UUID}
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
class GameLobby private(
|
class GameLobby private(
|
||||||
val logic: GameLogic,
|
val logic: GameLogic,
|
||||||
@@ -25,10 +29,11 @@ class GameLobby private(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val maxPlayers: Int
|
val maxPlayers: Int
|
||||||
) extends EventListener {
|
) extends EventListener {
|
||||||
logic.addListener(this)
|
|
||||||
logic.createSession()
|
|
||||||
|
|
||||||
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||||
|
logic.addListener(this)
|
||||||
|
logic.createSession()
|
||||||
|
|
||||||
def addUser(user: User): UserSession = {
|
def addUser(user: User): UserSession = {
|
||||||
if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
|
if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
|
||||||
@@ -36,9 +41,12 @@ class GameLobby private(
|
|||||||
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
||||||
val userSession = new UserSession(
|
val userSession = new UserSession(
|
||||||
user = user,
|
user = user,
|
||||||
host = false
|
host = false,
|
||||||
|
gameLobby = this
|
||||||
)
|
)
|
||||||
users += (user.id -> userSession)
|
users += (user.id -> userSession)
|
||||||
|
PodManager.registerUserToGame(user, id)
|
||||||
|
logic.invoke(LobbyUpdateEvent())
|
||||||
userSession
|
userSession
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +54,13 @@ class GameLobby private(
|
|||||||
event match {
|
event match {
|
||||||
case event: PlayerEvent =>
|
case event: PlayerEvent =>
|
||||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||||
|
case event: UserEvent =>
|
||||||
|
users.get(event.userId).foreach(session => session.updatePlayer(event))
|
||||||
case event: GameStateChangeEvent =>
|
case event: GameStateChangeEvent =>
|
||||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
case event: SessionClosed =>
|
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
|
||||||
case event: SimpleEvent =>
|
case event: SimpleEvent =>
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
}
|
}
|
||||||
@@ -60,6 +68,7 @@ class GameLobby private(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the game if the user is the host.
|
* Start the game if the user is the host.
|
||||||
|
*
|
||||||
* @param user the user who wants to start the game.
|
* @param user the user who wants to start the game.
|
||||||
*/
|
*/
|
||||||
def startGame(user: User): Unit = {
|
def startGame(user: User): Unit = {
|
||||||
@@ -86,18 +95,37 @@ class GameLobby private(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the user from the game lobby.
|
* Remove the user from the game lobby.
|
||||||
|
*
|
||||||
* @param user the user who wants to leave the game.
|
* @param user the user who wants to leave the game.
|
||||||
|
* @param kicked whether the user was kicked or left voluntarily.
|
||||||
*/
|
*/
|
||||||
def leaveGame(user: User): Unit = {
|
def leaveGame(userId: UUID, kicked: Boolean): Unit = {
|
||||||
val sessionOpt = users.get(user.id)
|
val sessionOpt = users.get(userId)
|
||||||
if (sessionOpt.isEmpty) {
|
if (sessionOpt.isEmpty) {
|
||||||
throw new NotInThisGameException("You are not in this game!")
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
}
|
}
|
||||||
users.remove(user.id)
|
if (sessionOpt.get.host) {
|
||||||
|
logic.invoke(SessionClosed())
|
||||||
|
for (session <- users.values) {
|
||||||
|
PodManager.unregisterUserFromGame(session.user)
|
||||||
|
}
|
||||||
|
users.clear()
|
||||||
|
PodManager.removeGame(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (kicked) {
|
||||||
|
logic.invoke(KickEvent(sessionOpt.get.user))
|
||||||
|
} else {
|
||||||
|
logic.invoke(LeftEvent(sessionOpt.get.user))
|
||||||
|
}
|
||||||
|
users.remove(userId)
|
||||||
|
PodManager.unregisterUserFromGame(sessionOpt.get.user)
|
||||||
|
logic.invoke(LobbyUpdateEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a card from the player's hand.
|
* Play a card from the player's hand.
|
||||||
|
*
|
||||||
* @param userSession the user session of the player.
|
* @param userSession the user session of the player.
|
||||||
* @param cardIndex the index of the card in the player's hand.
|
* @param cardIndex the index of the card in the player's hand.
|
||||||
*/
|
*/
|
||||||
@@ -115,85 +143,6 @@ class GameLobby private(
|
|||||||
logic.playerInputLogic.receivedCard(card)
|
logic.playerInputLogic.receivedCard(card)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Play a card from the player's hand while in dog life or skip the round.
|
|
||||||
* @param userSession the user session of the player.
|
|
||||||
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
|
||||||
*/
|
|
||||||
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
|
||||||
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
|
||||||
if (!player.isInDogLife) {
|
|
||||||
throw new CantPlayCardException("You are not in dog life!")
|
|
||||||
}
|
|
||||||
if (cardIndex == -1) {
|
|
||||||
if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
|
|
||||||
throw new CantPlayCardException("You can't skip this round!")
|
|
||||||
}
|
|
||||||
logic.playerInputLogic.receivedDog(None)
|
|
||||||
}
|
|
||||||
val hand = getHand(player)
|
|
||||||
val card = hand.cards(cardIndex)
|
|
||||||
userSession.resetCanInteract()
|
|
||||||
logic.playerInputLogic.receivedDog(Some(card))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the trump suit for the round.
|
|
||||||
* @param userSession the user session of the player.
|
|
||||||
* @param trumpIndex the index of the trump suit.
|
|
||||||
*/
|
|
||||||
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
|
||||||
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
|
||||||
val trumpSuits = Suit.values.toList
|
|
||||||
val selectedTrump = trumpSuits(trumpIndex)
|
|
||||||
userSession.resetCanInteract()
|
|
||||||
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param userSession
|
|
||||||
* @param tieNumber
|
|
||||||
*/
|
|
||||||
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
|
||||||
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
|
||||||
userSession.resetCanInteract()
|
|
||||||
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//-------------------
|
|
||||||
|
|
||||||
def getUserSession(userId: UUID): UserSession = {
|
|
||||||
val sessionOpt = users.get(userId)
|
|
||||||
if (sessionOpt.isEmpty) {
|
|
||||||
throw new NotInThisGameException("You are not in this game!")
|
|
||||||
}
|
|
||||||
sessionOpt.get
|
|
||||||
}
|
|
||||||
|
|
||||||
def getPlayerByUser(user: User): AbstractPlayer = {
|
|
||||||
getPlayerBySession(getUserSession(user.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
|
||||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
|
||||||
if (playerOption.isEmpty) {
|
|
||||||
throw new NotInThisGameException("You are not in this game!")
|
|
||||||
}
|
|
||||||
playerOption.get
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
|
||||||
if (!userSession.lock.isHeldByCurrentThread) {
|
|
||||||
throw new IllegalStateException("The user session is not locked!")
|
|
||||||
}
|
|
||||||
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
|
||||||
throw new NotInteractableException("You can't play a card!")
|
|
||||||
}
|
|
||||||
getPlayerBySession(userSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getHand(player: AbstractPlayer): Hand = {
|
private def getHand(player: AbstractPlayer): Hand = {
|
||||||
val handOption = player.currentHand()
|
val handOption = player.currentHand()
|
||||||
if (handOption.isEmpty) {
|
if (handOption.isEmpty) {
|
||||||
@@ -202,14 +151,6 @@ class GameLobby private(
|
|||||||
handOption.get
|
handOption.get
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getMatch: Match = {
|
|
||||||
val matchOpt = logic.getCurrentMatch
|
|
||||||
if (matchOpt.isEmpty) {
|
|
||||||
throw new IllegalStateException("No match is currently running!")
|
|
||||||
}
|
|
||||||
matchOpt.get
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getRound: Round = {
|
private def getRound: Round = {
|
||||||
val roundOpt = logic.getCurrentRound
|
val roundOpt = logic.getCurrentRound
|
||||||
if (roundOpt.isEmpty) {
|
if (roundOpt.isEmpty) {
|
||||||
@@ -226,6 +167,158 @@ class GameLobby private(
|
|||||||
trickOpt.get
|
trickOpt.get
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a card from the player's hand while in dog life or skip the round.
|
||||||
|
*
|
||||||
|
* @param userSession the user session of the player.
|
||||||
|
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||||
|
*/
|
||||||
|
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
||||||
|
if (!player.isInDogLife) {
|
||||||
|
throw new CantPlayCardException("You are not in dog life!")
|
||||||
|
}
|
||||||
|
if (cardIndex == -1) {
|
||||||
|
if (MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
|
||||||
|
throw new CantPlayCardException("You can't skip this round!")
|
||||||
|
}
|
||||||
|
logic.playerInputLogic.receivedDog(None)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val hand = getHand(player)
|
||||||
|
val card = hand.cards(cardIndex)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerInputLogic.receivedDog(Some(card))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the trump suit for the round.
|
||||||
|
*
|
||||||
|
* @param userSession the user session of the player.
|
||||||
|
* @param trumpIndex the index of the trump suit.
|
||||||
|
*/
|
||||||
|
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
||||||
|
val trumpSuits = Suit.values.toList
|
||||||
|
val selectedTrump = trumpSuits(trumpIndex)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------
|
||||||
|
|
||||||
|
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||||
|
if (!userSession.lock.isHeldByCurrentThread) {
|
||||||
|
throw new IllegalStateException("The user session is not locked!")
|
||||||
|
}
|
||||||
|
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||||
|
throw new NotInteractableException("You can't play a card!")
|
||||||
|
}
|
||||||
|
getPlayerBySession(userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param userSession
|
||||||
|
* @param tieNumber
|
||||||
|
*/
|
||||||
|
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
def returnToLobby(userSession: UserSession): Unit = {
|
||||||
|
if (!users.contains(userSession.id)) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
val session = users(userSession.id)
|
||||||
|
if (session != userSession) {
|
||||||
|
throw new IllegalArgumentException("User session does not match!")
|
||||||
|
}
|
||||||
|
if (!session.host)
|
||||||
|
throw new NotHostException("Only the host can return to the lobby!")
|
||||||
|
logic.createSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||||
|
getPlayerBySession(getUserSession(user.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUserSession(userId: UUID): UserSession = {
|
||||||
|
val sessionOpt = users.get(userId)
|
||||||
|
if (sessionOpt.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
sessionOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||||
|
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||||
|
if (playerOption.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
playerOption.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getMatch: Match = {
|
||||||
|
val matchOpt = logic.getCurrentMatch
|
||||||
|
if (matchOpt.isEmpty) {
|
||||||
|
throw new IllegalStateException("No match is currently running!")
|
||||||
|
}
|
||||||
|
matchOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPlayers: mutable.Map[UUID, UserSession] = {
|
||||||
|
users.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogic: GameLogic = {
|
||||||
|
logic
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUsers: Set[User] = {
|
||||||
|
users.values.map(d => d.user).toSet
|
||||||
|
}
|
||||||
|
def getFinalRanking: List[(String, (Int, Int))] = {
|
||||||
|
Try {
|
||||||
|
val match1 = getMatch
|
||||||
|
if (!match1.isOver) {
|
||||||
|
List.empty
|
||||||
|
} else {
|
||||||
|
val winnerName = logic.getWinner.get.name
|
||||||
|
|
||||||
|
val allPlayerNames = match1.totalplayers.map(_.name)
|
||||||
|
val roundlist = match1.roundlist
|
||||||
|
|
||||||
|
val playerMetrics: Map[String, (Int, Int)] = allPlayerNames.map { name =>
|
||||||
|
val roundsWon = roundlist.count { round =>
|
||||||
|
round.winner.exists(_.name == name)
|
||||||
|
}
|
||||||
|
val totalTricksWon = roundlist.flatMap(_.tricklist).count { trick =>
|
||||||
|
trick.winner.exists(_.name == name)
|
||||||
|
}
|
||||||
|
name -> (roundsWon, totalTricksWon)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
val winnerMetrics = playerMetrics(winnerName)
|
||||||
|
val remainingPlayersMetrics = playerMetrics.view.filterKeys(_ != winnerName).toList
|
||||||
|
|
||||||
|
val sortedRemainingPlayers = remainingPlayersMetrics.sortBy { case (_, (rounds, tricks)) =>
|
||||||
|
(-rounds, -tricks)
|
||||||
|
}
|
||||||
|
(winnerName, winnerMetrics) :: sortedRemainingPlayers
|
||||||
|
}
|
||||||
|
}.getOrElse(List())
|
||||||
|
}
|
||||||
|
|
||||||
|
private def transmitToAll(event: JsObject): Unit = {
|
||||||
|
users.values.foreach(session => {
|
||||||
|
session.websocketActor.foreach(act => act.transmitJsonToClient(event))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object GameLobby {
|
object GameLobby {
|
||||||
@@ -246,7 +339,8 @@ object GameLobby {
|
|||||||
)
|
)
|
||||||
lobby.users += (host.id -> new UserSession(
|
lobby.users += (host.id -> new UserSession(
|
||||||
user = host,
|
user = host,
|
||||||
host = true
|
host = true,
|
||||||
|
gameLobby = lobby
|
||||||
))
|
))
|
||||||
lobby
|
lobby
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import model.users.User
|
|||||||
trait SessionManager {
|
trait SessionManager {
|
||||||
|
|
||||||
def createSession(user: User): String
|
def createSession(user: User): String
|
||||||
|
|
||||||
|
|
||||||
def getUserBySession(sessionId: String): Option[User]
|
def getUserBySession(sessionId: String): Option[User]
|
||||||
|
|
||||||
def invalidateSession(sessionId: String): Unit
|
def invalidateSession(sessionId: String): Unit
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import model.users.User
|
|||||||
trait UserManager {
|
trait UserManager {
|
||||||
|
|
||||||
def addUser(name: String, password: String): Boolean
|
def addUser(name: String, password: String): Boolean
|
||||||
|
|
||||||
def authenticate(name: String, password: String): Option[User]
|
def authenticate(name: String, password: String): Option[User]
|
||||||
|
|
||||||
def userExists(name: String): Option[User]
|
def userExists(name: String): Option[User]
|
||||||
|
|
||||||
def userExistsById(id: Long): Option[User]
|
def userExistsById(id: Long): Option[User]
|
||||||
|
|
||||||
def removeUser(name: String): Boolean
|
def removeUser(name: String): Boolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.time.Instant
|
|||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.{Inject, Singleton}
|
import javax.inject.{Inject, Singleton}
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
|
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
|
||||||
@@ -44,16 +45,22 @@ class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
override def getUserBySession(sessionId: String): Option[User] = {
|
override def getUserBySession(sessionId: String): Option[User] = {
|
||||||
//TODO verify JWT token instead of looking up in cache
|
|
||||||
val cachedUser = cache.getIfPresent(sessionId)
|
val cachedUser = cache.getIfPresent(sessionId)
|
||||||
if (cachedUser != null) {
|
if (cachedUser != null) {
|
||||||
Some(cachedUser)
|
Some(cachedUser)
|
||||||
} else {
|
} else {
|
||||||
|
val result = Try {
|
||||||
val decoded = verifier.verify(sessionId)
|
val decoded = verifier.verify(sessionId)
|
||||||
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||||
user.foreach(u => cache.put(sessionId, u))
|
user.foreach(u => cache.put(sessionId, u))
|
||||||
user
|
user
|
||||||
}
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
result.get
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def invalidateSession(sessionId: String): Unit = {
|
override def invalidateSession(sessionId: String): Unit = {
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
|
|||||||
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
||||||
name = "Leon",
|
name = "Leon",
|
||||||
passwordHash = UserHash.hashPW("password123")
|
passwordHash = UserHash.hashPW("password123")
|
||||||
|
),
|
||||||
|
"Jakob" -> User(
|
||||||
|
internalId = 2L,
|
||||||
|
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
name = "Jakob",
|
||||||
|
passwordHash = UserHash.hashPW("password123")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import java.util.UUID
|
|||||||
trait PlayerSession {
|
trait PlayerSession {
|
||||||
|
|
||||||
def id: UUID
|
def id: UUID
|
||||||
|
|
||||||
def name: String
|
def name: String
|
||||||
|
|
||||||
def updatePlayer(event: SimpleEvent): Unit
|
def updatePlayer(event: SimpleEvent): Unit
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package model.sessions
|
|||||||
|
|
||||||
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||||
import de.knockoutwhist.utils.events.SimpleEvent
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
import model.users.User
|
import model.users.User
|
||||||
|
import play.api.libs.json.{JsObject, JsValue}
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) extends PlayerSession {
|
||||||
var canInteract: Option[InteractionType] = None
|
|
||||||
val lock: ReentrantLock = ReentrantLock()
|
val lock: ReentrantLock = ReentrantLock()
|
||||||
|
var canInteract: Option[InteractionType] = None
|
||||||
|
var websocketActor: Option[UserWebsocketActor] = None
|
||||||
|
|
||||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||||
event match {
|
event match {
|
||||||
@@ -22,6 +26,8 @@ class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
|||||||
else canInteract = Some(InteractionType.Card)
|
else canInteract = Some(InteractionType.Card)
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
|
websocketActor.foreach(_.solveRequests())
|
||||||
|
websocketActor.foreach(_.transmitEventToClient(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
override def id: UUID = user.id
|
override def id: UUID = user.id
|
||||||
@@ -32,4 +38,47 @@ class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
|||||||
canInteract = None
|
canInteract = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def handleWebResponse(eventType: String, data: JsObject): Unit = {
|
||||||
|
eventType match {
|
||||||
|
case "ping" =>
|
||||||
|
// No action needed for Ping
|
||||||
|
()
|
||||||
|
case "StartGame" =>
|
||||||
|
gameLobby.startGame(user)
|
||||||
|
case "PlayCard" =>
|
||||||
|
val maybeCardIndex: Option[Int] = (data \ "cardindex").asOpt[Int]
|
||||||
|
maybeCardIndex match {
|
||||||
|
case Some(index) =>
|
||||||
|
val session = gameLobby.getUserSession(user.id)
|
||||||
|
gameLobby.playCard(session, index)
|
||||||
|
case None =>
|
||||||
|
println("Card Index not found or is not a number." + data)
|
||||||
|
}
|
||||||
|
case "PickTrumpsuit" =>
|
||||||
|
val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int]
|
||||||
|
maybeSuitIndex match {
|
||||||
|
case Some(index) =>
|
||||||
|
val session = gameLobby.getUserSession(user.id)
|
||||||
|
gameLobby.selectTrump(session, index)
|
||||||
|
case None =>
|
||||||
|
println("Card Index not found or is not a number.")
|
||||||
|
}
|
||||||
|
case "KickPlayer" =>
|
||||||
|
val maybePlayerId: Option[String] = (data \ "playerId").asOpt[String]
|
||||||
|
maybePlayerId match {
|
||||||
|
case Some(id) =>
|
||||||
|
val playerUUID = UUID.fromString(id)
|
||||||
|
gameLobby.leaveGame(playerUUID, true)
|
||||||
|
case None =>
|
||||||
|
println("Player ID not found or is not a valid UUID.")
|
||||||
|
}
|
||||||
|
case "ReturnToLobby" =>
|
||||||
|
gameLobby.returnToLobby(this)
|
||||||
|
case "LeaveGame" =>
|
||||||
|
gameLobby.leaveGame(user.id, false)
|
||||||
|
case _ =>
|
||||||
|
println("Unknown event type: " + eventType + " with data: " + data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
133
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
133
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package model.sessions
|
||||||
|
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import org.apache.pekko.actor.{Actor, ActorRef}
|
||||||
|
import play.api.libs.json.{JsObject, JsValue, Json}
|
||||||
|
import util.WebsocketEventMapper
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
|
class UserWebsocketActor(
|
||||||
|
out: ActorRef,
|
||||||
|
session: UserSession
|
||||||
|
) extends Actor {
|
||||||
|
|
||||||
|
private val requests: mutable.Map[String, String] = mutable.Map()
|
||||||
|
|
||||||
|
{
|
||||||
|
session.lock.lock()
|
||||||
|
if (session.websocketActor.isDefined) {
|
||||||
|
val otherWebsocket = session.websocketActor.get
|
||||||
|
otherWebsocket.transmitTextToClient("Error: Multiple websocket connections detected. Closing your connection.")
|
||||||
|
context.stop(otherWebsocket.self)
|
||||||
|
transmitTextToClient("Previous websocket connection closed. You are now connected.")
|
||||||
|
}
|
||||||
|
session.websocketActor = Some(this)
|
||||||
|
session.lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case msg: String =>
|
||||||
|
val jsonObject = Try {
|
||||||
|
Json.parse(msg)
|
||||||
|
}
|
||||||
|
Try {
|
||||||
|
jsonObject match {
|
||||||
|
case Success(value) =>
|
||||||
|
handle(value)
|
||||||
|
case Failure(exception) =>
|
||||||
|
transmitTextToClient(s"Error parsing JSON: ${exception.getMessage}")
|
||||||
|
}
|
||||||
|
}.failed.foreach(
|
||||||
|
ex => transmitTextToClient(s"Error handling message: ${ex.getMessage}")
|
||||||
|
)
|
||||||
|
case other =>
|
||||||
|
}
|
||||||
|
|
||||||
|
private def transmitTextToClient(text: String): Unit = {
|
||||||
|
out ! text
|
||||||
|
}
|
||||||
|
|
||||||
|
private def handle(json: JsValue): Unit = {
|
||||||
|
session.lock.lock()
|
||||||
|
val idOpt = (json \ "id").asOpt[String]
|
||||||
|
if (idOpt.isEmpty) {
|
||||||
|
transmitJsonToClient(Json.obj(
|
||||||
|
"status" -> "error",
|
||||||
|
"error" -> "Missing 'id' field"
|
||||||
|
))
|
||||||
|
session.lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val id = idOpt.get
|
||||||
|
val eventOpt = (json \ "event").asOpt[String]
|
||||||
|
if (eventOpt.isEmpty) {
|
||||||
|
transmitJsonToClient(Json.obj(
|
||||||
|
"id" -> id,
|
||||||
|
"event" -> null,
|
||||||
|
"status" -> "error",
|
||||||
|
"error" -> "Missing 'event' field"
|
||||||
|
))
|
||||||
|
session.lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val statusOpt = (json \ "status").asOpt[String]
|
||||||
|
if (statusOpt.isDefined) {
|
||||||
|
session.lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val event = eventOpt.get
|
||||||
|
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
|
||||||
|
requests += (id -> event)
|
||||||
|
val result = Try {
|
||||||
|
session.handleWebResponse(event, data)
|
||||||
|
}
|
||||||
|
if (!requests.contains(id)) {
|
||||||
|
session.lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requests -= id
|
||||||
|
if (result.isSuccess) {
|
||||||
|
transmitJsonToClient(Json.obj(
|
||||||
|
"id" -> id,
|
||||||
|
"event" -> event,
|
||||||
|
"status" -> "success"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
transmitJsonToClient(Json.obj(
|
||||||
|
"id" -> id,
|
||||||
|
"event" -> event,
|
||||||
|
"status" -> "error",
|
||||||
|
"error" -> result.failed.get.getMessage
|
||||||
|
))
|
||||||
|
}
|
||||||
|
session.lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
def transmitJsonToClient(jsonObj: JsValue): Unit = {
|
||||||
|
transmitTextToClient(jsonObj.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
def transmitEventToClient(event: SimpleEvent): Unit = {
|
||||||
|
transmitJsonToClient(WebsocketEventMapper.toJson(event, session))
|
||||||
|
}
|
||||||
|
|
||||||
|
def solveRequests(): Unit = {
|
||||||
|
if (!session.lock.isHeldByCurrentThread)
|
||||||
|
return;
|
||||||
|
if (requests.isEmpty)
|
||||||
|
return;
|
||||||
|
val pendingRequests = requests.toMap
|
||||||
|
requests.clear()
|
||||||
|
pendingRequests.foreach { case (id, event) =>
|
||||||
|
transmitJsonToClient(Json.obj(
|
||||||
|
"id" -> id,
|
||||||
|
"event" -> event,
|
||||||
|
"status" -> "success"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package model.users
|
package model.users
|
||||||
|
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
case class User(
|
case class User(
|
||||||
@@ -16,5 +17,4 @@ case class User(
|
|||||||
private def withPasswordHash(newPasswordHash: String): User = {
|
private def withPasswordHash(newPasswordHash: String): User = {
|
||||||
this.copy(passwordHash = newPasswordHash)
|
this.copy(passwordHash = newPasswordHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
9
knockoutwhistweb/app/modules/GatewayModule.scala
Normal file
9
knockoutwhistweb/app/modules/GatewayModule.scala
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package modules
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule
|
||||||
|
|
||||||
|
class GatewayModule extends AbstractModule {
|
||||||
|
override def configure(): Unit = {
|
||||||
|
bind(classOf[GatewayModule]).asEagerSingleton()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,28 @@ import javax.inject.*
|
|||||||
@Singleton
|
@Singleton
|
||||||
class JwtKeyProvider @Inject()(config: Configuration) {
|
class JwtKeyProvider @Inject()(config: Configuration) {
|
||||||
|
|
||||||
private def cleanPem(pem: String): String =
|
val publicKey: RSAPublicKey = {
|
||||||
pem.replaceAll("-----BEGIN (.*)-----", "")
|
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
||||||
.replaceAll("-----END (.*)-----", "")
|
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
||||||
.replaceAll("\\s", "")
|
|
||||||
|
pemOpt.orElse(fileOpt.map { path =>
|
||||||
|
new String(Files.readAllBytes(Paths.get(path)))
|
||||||
|
}) match {
|
||||||
|
case Some(pem) => loadPublicKeyFromPem(pem)
|
||||||
|
case None => throw new RuntimeException("No RSA public key configured.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val privateKey: RSAPrivateKey = {
|
||||||
|
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
||||||
|
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
||||||
|
|
||||||
|
pemOpt.orElse(fileOpt.map { path =>
|
||||||
|
new String(Files.readAllBytes(Paths.get(path)))
|
||||||
|
}) match {
|
||||||
|
case Some(pem) => loadPrivateKeyFromPem(pem)
|
||||||
|
case None => throw new RuntimeException("No RSA private key configured.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
||||||
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||||
@@ -29,28 +47,9 @@ class JwtKeyProvider @Inject()(config: Configuration) {
|
|||||||
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
|
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
val publicKey: RSAPublicKey = {
|
private def cleanPem(pem: String): String =
|
||||||
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
pem.replaceAll("-----BEGIN (.*)-----", "")
|
||||||
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
.replaceAll("-----END (.*)-----", "")
|
||||||
|
.replaceAll("\\s", "")
|
||||||
pemOpt.orElse(fileOpt.map { path =>
|
|
||||||
new String(Files.readAllBytes(Paths.get(path)))
|
|
||||||
}) match {
|
|
||||||
case Some(pem) => loadPublicKeyFromPem(pem)
|
|
||||||
case None => throw new RuntimeException("No RSA public key configured.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val privateKey: RSAPrivateKey = {
|
|
||||||
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
|
||||||
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
|
||||||
|
|
||||||
pemOpt.orElse(fileOpt.map { path =>
|
|
||||||
new String(Files.readAllBytes(Paths.get(path)))
|
|
||||||
}) match {
|
|
||||||
case Some(pem) => loadPrivateKeyFromPem(pem)
|
|
||||||
case None => throw new RuntimeException("No RSA private key configured.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
|
import de.knockoutwhist.control.GameState
|
||||||
|
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, MainMenu, SelectTrump, TieBreak}
|
||||||
|
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
object GameUtil {
|
object GameUtil {
|
||||||
@@ -26,4 +29,15 @@ object GameUtil {
|
|||||||
code.toString()
|
code.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stateToTitle(gameState: GameState): String = {
|
||||||
|
gameState match {
|
||||||
|
case Lobby => "Lobby"
|
||||||
|
case MainMenu => "Main Menu"
|
||||||
|
case InGame => "In Game"
|
||||||
|
case SelectTrump => "Select Trump"
|
||||||
|
case TieBreak => "Tie Break"
|
||||||
|
case FinishedMatch => "Finished Match"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import de.knockoutwhist.cards.Card
|
|
||||||
import de.knockoutwhist.cards.CardValue.*
|
import de.knockoutwhist.cards.CardValue.*
|
||||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||||
|
import de.knockoutwhist.cards.{Card, Hand}
|
||||||
|
import play.api.libs.json.{JsArray, Json}
|
||||||
import play.twirl.api.Html
|
import play.twirl.api.Html
|
||||||
import scalafx.scene.image.Image
|
import scalafx.scene.image.Image
|
||||||
|
|
||||||
object WebUIUtils {
|
object WebUIUtils {
|
||||||
def cardtoImage(card: Card): Html = {
|
def cardtoImage(card: Card): Html = {
|
||||||
|
views.html.render.card.apply(cardToPath(card))(card.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
def cardToPath(card: Card): String = {
|
||||||
|
f"images/cards/${cardtoString(card)}.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
def cardtoString(card: Card): String = {
|
||||||
val s = card.suit match {
|
val s = card.suit match {
|
||||||
case Spades => "S"
|
case Spades => "S"
|
||||||
case Hearts => "H"
|
case Hearts => "H"
|
||||||
@@ -29,6 +38,50 @@ object WebUIUtils {
|
|||||||
case Three => "3"
|
case Three => "3"
|
||||||
case Two => "2"
|
case Two => "2"
|
||||||
}
|
}
|
||||||
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
f"$cv$s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stringToCard(cardStr: String): Card = {
|
||||||
|
val cv = cardStr.charAt(0) match {
|
||||||
|
case 'A' => Ace
|
||||||
|
case 'K' => King
|
||||||
|
case 'Q' => Queen
|
||||||
|
case 'J' => Jack
|
||||||
|
case 'T' => Ten
|
||||||
|
case '9' => Nine
|
||||||
|
case '8' => Eight
|
||||||
|
case '7' => Seven
|
||||||
|
case '6' => Six
|
||||||
|
case '5' => Five
|
||||||
|
case '4' => Four
|
||||||
|
case '3' => Three
|
||||||
|
case '2' => Two
|
||||||
|
}
|
||||||
|
val s = cardStr.charAt(1) match {
|
||||||
|
case 'S' => Spades
|
||||||
|
case 'H' => Hearts
|
||||||
|
case 'C' => Clubs
|
||||||
|
case 'D' => Diamonds
|
||||||
|
}
|
||||||
|
Card(cv, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a Hand to a JsArray of cards
|
||||||
|
* Per card it has the string and the index in the hand
|
||||||
|
* @param hand
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
def handToJson(hand: Hand): JsArray = {
|
||||||
|
val cards = hand.cards
|
||||||
|
JsArray(
|
||||||
|
cards.zipWithIndex.map { case (card, index) =>
|
||||||
|
Json.obj(
|
||||||
|
"idx" -> index,
|
||||||
|
"card" -> cardtoString(card)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
77
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
77
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import de.knockoutwhist.control.GameState
|
||||||
|
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import dto.subDTO.{CardDTO, HandDTO, PlayerDTO, PlayerQueueDTO, PodiumPlayerDTO, RoundDTO, TrickDTO, UserDTO}
|
||||||
|
import dto.{GameInfoDTO, LobbyInfoDTO, TieInfoDTO, TrumpInfoDTO, WonInfoDTO}
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsValue, Json, OFormat}
|
||||||
|
import tools.jackson.databind.json.JsonMapper
|
||||||
|
import tools.jackson.module.scala.ScalaModule
|
||||||
|
import util.mapper.*
|
||||||
|
|
||||||
|
object WebsocketEventMapper {
|
||||||
|
|
||||||
|
implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO]
|
||||||
|
implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO]
|
||||||
|
implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO]
|
||||||
|
implicit val queueFormat: OFormat[PlayerQueueDTO] = Json.format[PlayerQueueDTO]
|
||||||
|
implicit val podiumPlayerFormat: OFormat[PodiumPlayerDTO] = Json.format[PodiumPlayerDTO]
|
||||||
|
implicit val roundFormat: OFormat[RoundDTO] = Json.format[RoundDTO]
|
||||||
|
implicit val trickFormat: OFormat[TrickDTO] = Json.format[TrickDTO]
|
||||||
|
implicit val userFormat: OFormat[UserDTO] = Json.format[UserDTO]
|
||||||
|
|
||||||
|
implicit val gameInfoDTOFormat: OFormat[GameInfoDTO] = Json.format[GameInfoDTO]
|
||||||
|
implicit val lobbyFormat: OFormat[LobbyInfoDTO] = Json.format[LobbyInfoDTO]
|
||||||
|
implicit val tieInfoFormat: OFormat[TieInfoDTO] = Json.format[TieInfoDTO]
|
||||||
|
implicit val trumpInfoFormat: OFormat[TrumpInfoDTO] = Json.format[TrumpInfoDTO]
|
||||||
|
implicit val wonInfoDTOFormat: OFormat[WonInfoDTO] = Json.format[WonInfoDTO]
|
||||||
|
|
||||||
|
private var specialMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map()
|
||||||
|
|
||||||
|
private def registerCustomMapper[T <: SimpleEvent](mapper: SimpleEventMapper[T]): Unit = {
|
||||||
|
specialMappers = specialMappers + (mapper.id -> mapper.asInstanceOf[SimpleEventMapper[SimpleEvent]])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register all custom mappers here
|
||||||
|
registerCustomMapper(ReceivedHandEventMapper)
|
||||||
|
registerCustomMapper(GameStateEventMapper)
|
||||||
|
registerCustomMapper(CardPlayedEventMapper)
|
||||||
|
registerCustomMapper(NewRoundEventMapper)
|
||||||
|
registerCustomMapper(NewTrickEventMapper)
|
||||||
|
registerCustomMapper(TrickEndEventMapper)
|
||||||
|
registerCustomMapper(RequestCardEventMapper)
|
||||||
|
registerCustomMapper(LobbyUpdateEventMapper)
|
||||||
|
registerCustomMapper(LeftEventMapper)
|
||||||
|
registerCustomMapper(KickEventMapper)
|
||||||
|
registerCustomMapper(SessionClosedMapper)
|
||||||
|
registerCustomMapper(TurnEventMapper)
|
||||||
|
|
||||||
|
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
|
||||||
|
val data: Option[JsValue] = if (specialMappers.contains(obj.id)) {
|
||||||
|
Some(specialMappers(obj.id).toJson(obj, session))
|
||||||
|
}else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Json.obj(
|
||||||
|
"id" -> ("request-" + java.util.UUID.randomUUID().toString),
|
||||||
|
"event" -> obj.id,
|
||||||
|
"state" -> session.gameLobby.getLogic.getCurrentState.toString,
|
||||||
|
"stateData" -> stateToJson(session),
|
||||||
|
"data" -> data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def stateToJson(session: UserSession): JsValue = {
|
||||||
|
session.gameLobby.getLogic.getCurrentState match {
|
||||||
|
case Lobby => Json.toJson(LobbyInfoDTO(session.gameLobby, session.user))
|
||||||
|
case InGame => Json.toJson(GameInfoDTO(session.gameLobby, session.user))
|
||||||
|
case SelectTrump => Json.toJson(TrumpInfoDTO(session.gameLobby, session.user))
|
||||||
|
case TieBreak => Json.toJson(TieInfoDTO(session.gameLobby, session.user))
|
||||||
|
case FinishedMatch => Json.toJson(WonInfoDTO(session.gameLobby, session.user))
|
||||||
|
case _ => Json.obj()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.CardPlayedEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||||
|
import util.WebUIUtils
|
||||||
|
|
||||||
|
object CardPlayedEventMapper extends SimpleEventMapper[CardPlayedEvent]{
|
||||||
|
|
||||||
|
override def id: String = "CardPlayedEvent"
|
||||||
|
|
||||||
|
override def toJson(event: CardPlayedEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"firstCard" -> (if (event.trick.firstCard.isDefined) WebUIUtils.cardtoString(event.trick.firstCard.get) else "BLANK"),
|
||||||
|
"playedCards" -> JsArray(event.trick.cards.map { case (card, player) =>
|
||||||
|
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
|
||||||
|
}.toList)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
knockoutwhistweb/app/util/mapper/GameStateEventMapper.scala
Normal file
19
knockoutwhistweb/app/util/mapper/GameStateEventMapper.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import controllers.IngameController
|
||||||
|
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
import util.GameUtil
|
||||||
|
|
||||||
|
object GameStateEventMapper extends SimpleEventMapper[GameStateChangeEvent] {
|
||||||
|
|
||||||
|
override def id: String = "GameStateChangeEvent"
|
||||||
|
|
||||||
|
override def toJson(event: GameStateChangeEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"title" -> ("Knockout Whist - " + GameUtil.stateToTitle(event.newState)),
|
||||||
|
"content" -> IngameController.returnInnerHTML(session.gameLobby, event.newState, session.user).toString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
knockoutwhistweb/app/util/mapper/KickEventMapper.scala
Normal file
19
knockoutwhistweb/app/util/mapper/KickEventMapper.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import controllers.routes
|
||||||
|
import events.KickEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object KickEventMapper extends SimpleEventMapper[KickEvent] {
|
||||||
|
|
||||||
|
override def id: String = "KickEvent"
|
||||||
|
|
||||||
|
override def toJson(event: KickEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"url" -> routes.MainMenuController.mainMenu().url,
|
||||||
|
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
knockoutwhistweb/app/util/mapper/LeftEventMapper.scala
Normal file
19
knockoutwhistweb/app/util/mapper/LeftEventMapper.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import controllers.routes
|
||||||
|
import events.{KickEvent, LeftEvent}
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object LeftEventMapper extends SimpleEventMapper[LeftEvent] {
|
||||||
|
|
||||||
|
override def id: String = "LeftEvent"
|
||||||
|
|
||||||
|
override def toJson(event: LeftEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"url" -> routes.MainMenuController.mainMenu().url,
|
||||||
|
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import events.LobbyUpdateEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||||
|
|
||||||
|
object LobbyUpdateEventMapper extends SimpleEventMapper[LobbyUpdateEvent] {
|
||||||
|
|
||||||
|
override def id: String = "LobbyUpdateEvent"
|
||||||
|
|
||||||
|
override def toJson(event: LobbyUpdateEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"host" -> session.host,
|
||||||
|
"maxPlayers" -> session.gameLobby.maxPlayers,
|
||||||
|
"players" -> JsArray(session.gameLobby.getPlayers.values.map(player => {
|
||||||
|
Json.obj(
|
||||||
|
"id" -> player.id,
|
||||||
|
"name" -> player.name,
|
||||||
|
"self" -> (player.id == session.user.id)
|
||||||
|
)
|
||||||
|
}).toList)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.NewRoundEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
|
||||||
|
override def id: String = "NewRoundEvent"
|
||||||
|
|
||||||
|
override def toJson(event: NewRoundEvent, session: UserSession): JsObject = {
|
||||||
|
val gameLobby = session.gameLobby
|
||||||
|
Json.obj(
|
||||||
|
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
|
||||||
|
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.NewTrickEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
|
||||||
|
override def id: String = "NewTrickEvent"
|
||||||
|
|
||||||
|
override def toJson(event: NewTrickEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.player.ReceivedHandEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
import util.WebUIUtils
|
||||||
|
|
||||||
|
object ReceivedHandEventMapper extends SimpleEventMapper[ReceivedHandEvent] {
|
||||||
|
|
||||||
|
override def id: String = "ReceivedHandEvent"
|
||||||
|
override def toJson(event: ReceivedHandEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"dog" -> event.player.isInDogLife,
|
||||||
|
"hand" -> event.player.currentHand().map(hand => WebUIUtils.handToJson(hand))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.player.RequestCardEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
|
||||||
|
override def id: String = "RequestCardEvent"
|
||||||
|
|
||||||
|
override def toJson(event: RequestCardEvent, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"player" -> event.player.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
knockoutwhistweb/app/util/mapper/RoundEndEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/RoundEndEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import controllers.routes
|
||||||
|
import de.knockoutwhist.events.global.RoundEndEvent
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object RoundEndEventMapper extends SimpleEventMapper[RoundEndEvent] {
|
||||||
|
|
||||||
|
override def id: String = "RoundEndEvent"
|
||||||
|
|
||||||
|
override def toJson(event: RoundEndEvent, session: UserSession): JsObject = {
|
||||||
|
|
||||||
|
Json.obj(
|
||||||
|
"player" -> event.winner.name,
|
||||||
|
"tricks" -> event.amountOfTricks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala
Normal file
19
knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import controllers.routes
|
||||||
|
import de.knockoutwhist.events.global.SessionClosed
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object SessionClosedMapper extends SimpleEventMapper[SessionClosed] {
|
||||||
|
|
||||||
|
override def id: String = "SessionClosed"
|
||||||
|
|
||||||
|
override def toJson(event: SessionClosed, session: UserSession): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"url" -> routes.MainMenuController.mainMenu().url,
|
||||||
|
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala
Normal file
13
knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.JsObject
|
||||||
|
|
||||||
|
trait SimpleEventMapper[T <: SimpleEvent] {
|
||||||
|
|
||||||
|
def id: String
|
||||||
|
def toJson(event: T, session: UserSession): JsObject
|
||||||
|
|
||||||
|
}
|
||||||
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.TrickEndEvent
|
||||||
|
import de.knockoutwhist.rounds.Trick
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
|
||||||
|
override def id: String = "TrickEndEvent"
|
||||||
|
|
||||||
|
override def toJson(event: TrickEndEvent, session: UserSession): JsObject = {
|
||||||
|
val gameLobby = session.gameLobby
|
||||||
|
Json.obj(
|
||||||
|
"playerwon" -> event.winner.name,
|
||||||
|
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
|
||||||
|
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
knockoutwhistweb/app/util/mapper/TurnEventMapper.scala
Normal file
35
knockoutwhistweb/app/util/mapper/TurnEventMapper.scala
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.TurnEvent
|
||||||
|
import de.knockoutwhist.player.AbstractPlayer
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||||
|
|
||||||
|
object TurnEventMapper extends SimpleEventMapper[TurnEvent] {
|
||||||
|
|
||||||
|
override def id: String = "TurnEvent"
|
||||||
|
|
||||||
|
override def toJson(event: TurnEvent, session: UserSession): JsObject = {
|
||||||
|
val nextPlayers = if (session.gameLobby.logic.getPlayerQueue.isEmpty) {
|
||||||
|
Json.arr()
|
||||||
|
} else {
|
||||||
|
val queue = session.gameLobby.logic.getPlayerQueue.get
|
||||||
|
JsArray(
|
||||||
|
queue.duplicate().map(player => mapPlayer(player)).toList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Json.obj(
|
||||||
|
"currentPlayer" -> mapPlayer(event.player),
|
||||||
|
"nextPlayers" -> nextPlayers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def mapPlayer(player: AbstractPlayer): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"name" -> player.name,
|
||||||
|
"dog" -> player.isInDogLife
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
108
knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
Normal file
108
knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
|
<div class="lobby-background vh-100 d-flex align-items-start justify-content-center pt-5">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8 col-xl-6">
|
||||||
|
<div class="card shadow-lg mb-5 text-center bg-white border-0 rounded-4">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h1 class="card-title display-5 fw-bold text-success mb-3">Match Over!</h1>
|
||||||
|
<p class="fs-4 text-muted">Congratulations to the winner:</p>
|
||||||
|
<h2 class="display-3 fw-bolder mb-4 text-primary">
|
||||||
|
@gamelobby.getLogic.getWinner.get.name
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow mb-5 border-0 rounded-4 overflow-hidden">
|
||||||
|
<div class="card-header bg-dark text-white text-center fs-5 fw-semibold">
|
||||||
|
Final Standings
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center p-2 text-uppercase fw-bold border-bottom bg-light">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
Player
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row gap-3">
|
||||||
|
<span class="fs-6 text-dark text-center" style="width: 5rem;">Rounds won</span>
|
||||||
|
<span class="fs-6 text-dark text-center" style="width: 5rem;">Tricks won</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@gamelobby.getFinalRanking.zipWithIndex.map { case ((playerName, (wonRounds, tricksWon)), index) =>
|
||||||
|
@defining(index + 1) { rank =>
|
||||||
|
<div class="d-flex justify-content-between align-items-center p-3 border-bottom @if(rank == 1){bg-success-subtle fw-bold}">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="badge @if(rank == 1){bg-warning text-dark fs-6} else {bg-secondary} rounded-pill me-3">#@rank</span>
|
||||||
|
@playerName
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row gap-3">
|
||||||
|
<span class="fs-6 text-muted text-center" style="width: 5rem;">@wonRounds</span>
|
||||||
|
<span class="fs-6 text-muted text-center" style="width: 5rem;">@tricksWon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(gamelobby.getFinalRanking.isEmpty) {
|
||||||
|
<div class="p-3 text-center text-muted">No final scores available.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@if(user.isDefined && gamelobby.getUserSession(user.get.id).host) {
|
||||||
|
<div class="col-12 text-center mt-4">
|
||||||
|
<div class="btn btn-success btn-lg shadow" onclick="handleReturnToLobby()">
|
||||||
|
Return to Lobby
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="col-12 text-center mt-4">
|
||||||
|
<div class="text-primary">
|
||||||
|
<div class="spinner-grow text-primary mt-1" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 fs-6 fw-semibold">
|
||||||
|
Waiting for the Host to continue...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function fireConfetti() {
|
||||||
|
let duration = 3 * 1000;
|
||||||
|
let animationEnd = Date.now() + duration;
|
||||||
|
let defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
||||||
|
|
||||||
|
function randomInRange(min, max) {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval = setInterval(function() {
|
||||||
|
let timeLeft = animationEnd - Date.now();
|
||||||
|
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
return clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
let particleCount = 50 * (timeLeft / duration);
|
||||||
|
|
||||||
|
// Left burst
|
||||||
|
confetti(Object.assign({}, defaults, {
|
||||||
|
particleCount,
|
||||||
|
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Right burst
|
||||||
|
confetti(Object.assign({}, defaults, {
|
||||||
|
particleCount,
|
||||||
|
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
|
||||||
|
}));
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
connectWebSocket();
|
||||||
|
fireConfetti();
|
||||||
|
</script>
|
||||||
@@ -1,50 +1,42 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
|
||||||
|
@import de.knockoutwhist.utils.Implicits.*
|
||||||
|
|
||||||
@main("Ingame") {
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
<div id="ingame" class="game-field game-field-background">
|
|
||||||
<h1>Knockout Whist</h1>
|
|
||||||
<div id="nextPlayers">
|
|
||||||
<p>Next Player:</p>
|
|
||||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
|
||||||
</div>
|
|
||||||
<div id="firstCard">
|
|
||||||
<div id="trumpsuit">
|
|
||||||
<p>Trumpsuit: </p>
|
|
||||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
|
||||||
</div>
|
|
||||||
<div id="firstCardObject">
|
|
||||||
<p>First Card</p>
|
|
||||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
|
||||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
|
||||||
} else {
|
|
||||||
@views.html.render.card.apply("images/cards/1B.png")("Blank Card")
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>@logic.getCurrentPlayer.get has to play a card!</p>
|
<div class="lobby-background vh-100">
|
||||||
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
|
<main class="game-field-background vh-100 ingame-side-shadow">
|
||||||
<p>Cards played</p>
|
<div class="py-5 container-xxl">
|
||||||
} else {
|
|
||||||
<p id="invisible">Cards played</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div id="cardsplayed">
|
<div class="row ms-4 me-4">
|
||||||
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
|
<div class="col-4 mt-5 text-start" id="turn-component"></div>
|
||||||
<div id="playedcardplayer">
|
|
||||||
<p>@player</p>
|
<div class="col-4 text-center">
|
||||||
@util.WebUIUtils.cardtoImage(cardplayed)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
<div id="score-table"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container"></div>
|
||||||
|
|
||||||
<p>Your cards</p>
|
|
||||||
<div id="playercards">
|
|
||||||
@for(card <- player.currentHand().get.cards) {
|
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-4 mt-5 text-end" id="game-info-component"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center g-2 mt-4 bottom-div" style="backdrop-filter: blur(4px);
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;">
|
||||||
|
<div id="player-hand-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
}
|
<script>
|
||||||
|
connectWebSocket()
|
||||||
|
canPlayCard = @gamelobby.logic.getCurrentPlayer.contains(player);
|
||||||
|
globalThis.initGameVueComponents()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,27 +1,68 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Selecting Trumpsuit...") {
|
|
||||||
<div id="selecttrumpsuit" class="game-field game-field-background">
|
<div id="selecttrumpsuit" class="game-field game-field-background">
|
||||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
<div class="ingame-stage blur-sides">
|
||||||
<h1>Knockout Whist</h1>
|
<div class="container py-4">
|
||||||
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
<div class="row justify-content-center">
|
||||||
<p>Available trumpsuits are displayed below:</p>
|
<div class="col-12">
|
||||||
<div id="playercards">
|
<div class="card shadow-sm">
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
<div class="card-header text-center">
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
<h3 class="mb-0">Select Trump Suit</h3>
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
</div>
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
<div class="card-body">
|
||||||
|
@if(gamelobby.logic.getCurrentMatch.isDefined) {
|
||||||
|
@if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||||
|
<div class="alert alert-info" role="alert" aria-live="polite">
|
||||||
|
You (@player.toString) won the last round. Choose the trump suit for the next round.
|
||||||
</div>
|
</div>
|
||||||
<p>Your cards</p>
|
|
||||||
|
|
||||||
<div id="playercards">
|
<div class="row justify-content-center col-auto mb-5">
|
||||||
@for(card <- player.currentHand().get.cards) {
|
<div class="col-auto handcard">
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="0" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
||||||
|
width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="1" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
||||||
|
width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="2" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
||||||
|
width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="3" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
||||||
|
width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-content-center ingame-cards-slide" id="card-slide">
|
||||||
|
@for(i <- player.currentHand().get.cards.indices) {
|
||||||
|
<div class="col-auto" style="border-radius: 6px">
|
||||||
|
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<h1>Knockout Whist</h1>
|
<div class="alert alert-warning" role="alert" aria-live="polite">
|
||||||
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
|
@gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name
|
||||||
|
is choosing a trumpsuit. The new round will start once a suit is picked.
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<script>
|
||||||
|
connectWebSocket()
|
||||||
|
</script>
|
||||||
@@ -1,27 +1,114 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Tie") {
|
|
||||||
<div id="tie" class="game-field game-field-background">
|
<div id="tie" class="game-field game-field-background">
|
||||||
<h1>Knockout Whist</h1>
|
<div class="ingame-stage blur-sides">
|
||||||
<p>The last Round was tied between
|
<div class="container py-4">
|
||||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
<div class="row justify-content-center">
|
||||||
@players
|
<div class="col-12 col-md-10 col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3 class="mb-0">Tie Break</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<p class="card-text">
|
||||||
|
The last round was tied between:
|
||||||
|
<span class="ms-1">
|
||||||
|
@for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
|
||||||
|
<span class="badge text-bg-secondary me-1">@players</span>
|
||||||
}
|
}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
</div>
|
||||||
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p>
|
|
||||||
} else {
|
@if(gamelobby.logic.playerTieLogic.currentTiePlayer().contains(player)) {
|
||||||
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p>
|
@defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
|
||||||
<p>Currently picked Cards:</p>
|
<div class="alert alert-info" role="alert" aria-live="polite">
|
||||||
<div id="cardsplayed">
|
Pick a number between 1 and @{
|
||||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
maxNum + 1
|
||||||
<div id="playedcardplayer">
|
}.
|
||||||
<p>@player</p>
|
The resulting card will be your card for the cut.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="tieNumber" class="col-form-label">Your number</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="number" id="tieNumber" class="form-control" name="tie" min="1" max="@{
|
||||||
|
maxNum + 1
|
||||||
|
}" placeholder="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button onclick="selectTie('@gamelobby.id')" class="btn btn-primary">
|
||||||
|
Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
||||||
|
|
||||||
|
<div id="cardsplayed" class="row g-3 justify-content-center">
|
||||||
|
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||||
|
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="card shadow-sm border-0 h-100 text-center">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
|
||||||
|
<div class="card-img-top">
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
@util.WebUIUtils.cardtoImage(card)
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info text-center" role="alert">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
No cards have been selected yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<div class="alert alert-warning" role="alert" aria-live="polite">
|
||||||
|
<strong>@gamelobby.logic.playerTieLogic.currentTiePlayer().get</strong>
|
||||||
|
is currently picking a number for the cut.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
||||||
|
|
||||||
|
<div id="cardsplayed" class="row g-3 justify-content-center">
|
||||||
|
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||||
|
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
||||||
|
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||||
|
<div class="card shadow-sm border-0 h-100 text-center">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
|
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
|
||||||
|
<div class="card-img-top">
|
||||||
|
@util.WebUIUtils.cardtoImage(card)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info text-center" role="alert">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
No cards have been selected yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<script>
|
||||||
|
connectWebSocket()
|
||||||
|
</script>
|
||||||
38
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
38
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||||
|
@import play.api.libs.json._
|
||||||
|
<div id="lobby-app-mount"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Initialisierung des momentanen Lobby Standes, welcher direkt gerendert werden muss.
|
||||||
|
|
||||||
|
const initialLobbyName = '@gamelobby.name';
|
||||||
|
const initialLobbyId = '@gamelobby.id';
|
||||||
|
|
||||||
|
const initialIsHost = @{user.map(u => gamelobby.getUserSession(u.id)).exists(_.host)};
|
||||||
|
const initialMaxPlayers = @{gamelobby.maxPlayers};
|
||||||
|
|
||||||
|
const initialPlayers = JSON.parse('@Html({
|
||||||
|
val currentUserId = user.map(_.id).getOrElse(java.util.UUID.randomUUID())
|
||||||
|
|
||||||
|
val playerListForVue = gamelobby.getUsers.toSeq.map { u =>
|
||||||
|
val isSelf = u.id == currentUserId
|
||||||
|
val playerDogStatus = false
|
||||||
|
|
||||||
|
Json.obj(
|
||||||
|
"id" -> u.id.toString,
|
||||||
|
"name" -> u.name,
|
||||||
|
"self" -> isSelf,
|
||||||
|
"dog" -> playerDogStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Json.stringify(Json.toJson(playerListForVue))
|
||||||
|
})');
|
||||||
|
connectWebSocket();
|
||||||
|
globalThis.initLobbyVueComponents(
|
||||||
|
initialLobbyName,
|
||||||
|
initialLobbyId,
|
||||||
|
initialIsHost,
|
||||||
|
initialMaxPlayers,
|
||||||
|
initialPlayers
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -1,27 +1,24 @@
|
|||||||
@()
|
@()
|
||||||
|
<div class="login-box">
|
||||||
@main("Login") {
|
|
||||||
<div class="login-box">
|
|
||||||
<div class="card login-card p-4">
|
<div class="card login-card p-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-4">Login</h3>
|
<h3 class="text-center mb-4 text-body">Login</h3>
|
||||||
|
<form onsubmit="login(); return false;">
|
||||||
<form action="@routes.UserController.login_Post()" method="post">
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Username</label>
|
<label for="username" class="form-label text-body">Username</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required>
|
<input type="text" class="form-control text-body" id="username" name="username" placeholder="Enter Username" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label for="password" class="form-label text-body">Password</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required>
|
<input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<a href="#" class="text-decoration-none">Forgot password?</a>
|
<a href="#" class="text-decoration-none">Forgot password?</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid" >
|
||||||
<button type="submit" class="btn btn-primary">Login</button>
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -32,10 +29,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
||||||
<div id="particles-js" style="background-color: rgb(182, 25, 36);
|
<script>
|
||||||
|
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||||
|
console.log('callback - particles.js config loaded');
|
||||||
|
});
|
||||||
|
disconnectWebSocket();
|
||||||
|
</script>
|
||||||
|
<div id="particles-js" style="background-color: rgb(11, 8, 8);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 50% 50%;"></div>
|
background-position: 50% 50%;"></div>
|
||||||
}
|
|
||||||
@@ -1,28 +1,36 @@
|
|||||||
@*
|
@*
|
||||||
* This template is called from the `index` template. This template
|
* This template is called from the `index` template. This template
|
||||||
* handles the rendering of the page header and body tags. It takes
|
* handles the rendering of the page header and body tags. It takes
|
||||||
* two arguments, a `String` for the title of the page and an `Html`
|
* two arguments, a `String` for the title of the page and an `Html`
|
||||||
* object to insert into the body of the page.
|
* object to insert into the body of the page.
|
||||||
*@
|
*@
|
||||||
@(title: String)(content: Html)
|
@(title: String)(content: Html)
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@* Here's where we render the page title `String`. *@
|
@* Here's where we render the page title `String`. *@
|
||||||
<title>@title</title>
|
<title>@title</title>
|
||||||
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
||||||
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@@1.6.0/dist/confetti.browser.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
||||||
|
<script src="@routes.Assets.versioned("javascripts/websocket.js")" type="text/javascript"></script>
|
||||||
|
<script src="@routes.Assets.versioned("javascripts/events.js")" type="text/javascript"></script>
|
||||||
|
<script src="@routes.Assets.versioned("javascripts/interact.js")" type="text/javascript"></script>
|
||||||
|
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
||||||
|
|
||||||
<body>
|
<body class="d-flex flex-column min-vh-100" id="main-body">
|
||||||
|
<div id="alerts-container"></div>
|
||||||
@* And here's where we render the `Html` object containing
|
@* And here's where we render the `Html` object containing
|
||||||
* the page content. *@
|
* the page content. *@
|
||||||
@content
|
@content
|
||||||
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
33
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
33
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
|
@navbar(user)
|
||||||
|
<main class="lobby-background flex-grow-1">
|
||||||
|
<div class="w-25 mx-auto">
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="lobbyname" class="form-label">Lobby-Name</label>
|
||||||
|
<input type="text" class="form-control" id="lobbyname" name="lobbyname" placeholder="Lobby 1" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="visibilityswitch" disabled>
|
||||||
|
<label class="form-check-label" for="visibilityswitch">public/private</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="playeramount" class="form-label">Playeramount:</label>
|
||||||
|
<input type="range" class="form-range text-body" min="2" max="7" value="2" id="playeramount" name="playeramount">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>2</span>
|
||||||
|
<span>3</span>
|
||||||
|
<span>4</span>
|
||||||
|
<span>5</span>
|
||||||
|
<span>6</span>
|
||||||
|
<span>7</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<div class="btn btn-success" onclick="createGameJS()">Create Game</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
disconnectWebSocket();
|
||||||
|
</script>
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
@(user: Option[model.users.User])
|
@(user: Option[model.users.User])
|
||||||
@main("Knockout Whist - Main Menu") {
|
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-drop-shadow">
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<div class="container d-flex justify-content-start">
|
||||||
<div class="container-fluid">
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navBar">
|
<div class="collapse navbar-collapse justify-content-center" id="navBar">
|
||||||
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a>
|
<a class="navbar-brand ms-auto" href="@routes.MainMenuController.mainMenu()">
|
||||||
|
<img src="@routes.Assets.versioned("images/logo.png")" alt="Logo" width="30" height="20" class="d-inline-block align-text-top">
|
||||||
|
KnockOutWhist
|
||||||
|
</a>
|
||||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||||
@if(user.isDefined) {
|
@if(user.isDefined) {
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="#">Create Game</a>
|
<a class="nav-link active" aria-current="page" onclick="navSpa('0', 'Knockout Whist - Create Game'); return false;" href="@routes.MainMenuController.mainMenu()">
|
||||||
|
Create Game</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="@routes.MainMenuController.rules()">Rules</a>
|
<a class="nav-link active" onclick="navSpa('1', 'Knockout Whist - Rules'); return false;" href="@routes.MainMenuController.rules()">
|
||||||
|
Rules</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="navbar-nav me-auto mb-2 mb-lg-0" method="post" action="@routes.MainMenuController.joinGame()">
|
<form class="navbar-nav me-auto mb-2 mb-lg-0" onsubmit="joinGame(); return false;">
|
||||||
<input class="form-control me-2" type="text" placeholder="Enter GameCode" name="gameId" aria-label="Join Game"/>
|
<input class="form-control me-2" type="text" placeholder="Enter GameCode" id="gameId" aria-label="Join Game"/>
|
||||||
<button class="btn btn-outline-success" type="submit">Join</button>
|
<button class="btn btn-outline-success" type="submit">Join</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,21 +40,16 @@
|
|||||||
<span class="ms-2">@user.get.name</span>
|
<span class="ms-2">@user.get.name</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
|
||||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Stats</a></li>
|
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">
|
||||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Settings</a></li>
|
Stats</a></li>
|
||||||
|
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">
|
||||||
|
Settings</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="@routes.UserController.logout()">Logout</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
} else {
|
|
||||||
<div class="d-flex ms-auto">
|
|
||||||
<a class="btn btn-outline-primary me-2" href="@routes.UserController.login()">Login</a>
|
|
||||||
<a class="btn btn-primary" href="@routes.UserController.login()">Sign Up</a>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,63 +1,180 @@
|
|||||||
@()
|
@(user: Option[model.users.User])
|
||||||
|
@navbar(user)
|
||||||
|
|
||||||
@main("Rules") {
|
<main class="lobby-background flex-grow-1">
|
||||||
<div id="rules" class="game-field game-field-background">
|
<div class="container my-4" style="max-width: 980px;">
|
||||||
<table>
|
<div class="card rules-card shadow-sm rounded-3 overflow-hidden">
|
||||||
<caption>Rules Overview and Equipment</caption>
|
<div class="card-header text-center py-3 border-0">
|
||||||
<thead>
|
<h3 class="mb-0 rules-title">Game Rules Overview</h3>
|
||||||
<tr>
|
</div>
|
||||||
<th>Section</th>
|
|
||||||
<th>Details</th>
|
<div class="card-body p-0">
|
||||||
</tr>
|
<style>
|
||||||
</thead>
|
|
||||||
<tbody>
|
</style>
|
||||||
<tr>
|
|
||||||
<td>Players</td>
|
<div class="accordion rules-accordion" id="rulesAccordion">
|
||||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
<div class="accordion-item">
|
||||||
</tr>
|
<h2 class="accordion-header" id="headingPlayers">
|
||||||
<tr>
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlayers" aria-expanded="true" aria-controls="collapsePlayers">
|
||||||
<td>Aim</td>
|
Players
|
||||||
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
</button>
|
||||||
</tr>
|
</h2>
|
||||||
<tr>
|
<div id="collapsePlayers" class="accordion-collapse collapse show" aria-labelledby="headingPlayers" data-bs-parent="#rulesAccordion">
|
||||||
<td>Equipment</td>
|
<div class="accordion-body">
|
||||||
<td>A standard 52-card pack is used.</td>
|
Two to seven players. The aim is to be the last player left in the game.
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<td>Card Ranks</td>
|
</div>
|
||||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
|
||||||
</tr>
|
<div class="accordion-item">
|
||||||
<tr>
|
<h2 class="accordion-header" id="headingAim">
|
||||||
<td>Deal (First Hand)</td>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAim" aria-expanded="false" aria-controls="collapseAim">
|
||||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
Aim
|
||||||
</tr>
|
</button>
|
||||||
<tr>
|
</h2>
|
||||||
<td>Deal (Subsequent Hands)</td>
|
<div id="collapseAim" class="accordion-collapse collapse" aria-labelledby="headingAim" data-bs-parent="#rulesAccordion">
|
||||||
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
<div class="accordion-body">
|
||||||
</tr>
|
To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks.
|
||||||
<tr>
|
</div>
|
||||||
<td>Play</td>
|
</div>
|
||||||
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
<div class="accordion-item">
|
||||||
<td>Winning a Trick</td>
|
<h2 class="accordion-header" id="headingEquipment">
|
||||||
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEquipment" aria-expanded="false" aria-controls="collapseEquipment">
|
||||||
</tr>
|
Equipment
|
||||||
<tr>
|
</button>
|
||||||
<td>Leading Trumps</td>
|
</h2>
|
||||||
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
<div id="collapseEquipment" class="accordion-collapse collapse" aria-labelledby="headingEquipment" data-bs-parent="#rulesAccordion">
|
||||||
</tr>
|
<div class="accordion-body">
|
||||||
<tr>
|
A standard 52-card pack is used.
|
||||||
<td>Knockout</td>
|
</div>
|
||||||
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
|
||||||
<td>Winning the Game</td>
|
<div class="accordion-item">
|
||||||
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
<h2 class="accordion-header" id="headingRanks">
|
||||||
</tr>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRanks" aria-expanded="false" aria-controls="collapseRanks">
|
||||||
</tbody>
|
Card Ranks
|
||||||
<td>Dog Life</td>
|
</button>
|
||||||
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
</h2>
|
||||||
</table>
|
<div id="collapseRanks" class="accordion-collapse collapse" aria-labelledby="headingRanks" data-bs-parent="#rulesAccordion">
|
||||||
</div>
|
<div class="accordion-body">
|
||||||
}
|
In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingDealFirst">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealFirst" aria-expanded="false" aria-controls="collapseDealFirst">
|
||||||
|
Deal (First Hand)
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseDealFirst" class="accordion-collapse collapse" aria-labelledby="headingDealFirst" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingDealSubsequent">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealSubsequent" aria-expanded="false" aria-controls="collapseDealSubsequent">
|
||||||
|
Deal (Subsequent Hands)
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseDealSubsequent" class="accordion-collapse collapse" aria-labelledby="headingDealSubsequent" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingPlay">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlay" aria-expanded="false" aria-controls="collapsePlay">
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapsePlay" class="accordion-collapse collapse" aria-labelledby="headingPlay" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingWinningTrick">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningTrick" aria-expanded="false" aria-controls="collapseWinningTrick">
|
||||||
|
Winning a Trick
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseWinningTrick" class="accordion-collapse collapse" aria-labelledby="headingWinningTrick" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingLeadingTrumps">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLeadingTrumps" aria-expanded="false" aria-controls="collapseLeadingTrumps">
|
||||||
|
Leading Trumps
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseLeadingTrumps" class="accordion-collapse collapse" aria-labelledby="headingLeadingTrumps" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingKnockout">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseKnockout" aria-expanded="false" aria-controls="collapseKnockout">
|
||||||
|
Knockout
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseKnockout" class="accordion-collapse collapse" aria-labelledby="headingKnockout" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingWinningGame">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningGame" aria-expanded="false" aria-controls="collapseWinningGame">
|
||||||
|
Winning the Game
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseWinningGame" class="accordion-collapse collapse" aria-labelledby="headingWinningGame" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingDogLife">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDogLife" aria-expanded="false" aria-controls="collapseDogLife">
|
||||||
|
Dog Life
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseDogLife" class="accordion-collapse collapse" aria-labelledby="headingDogLife" data-bs-parent="#rulesAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
The first player who takes no tricks is awarded a \"dog's life\". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the dog may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
disconnectWebSocket();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
@(src: String)(alt: String)
|
@(src: String)(alt: String)
|
||||||
<img src="@routes.Assets.versioned(src)" alt="@alt"/>
|
<img src="@routes.Assets.versioned(src)" alt="@alt"
|
||||||
@@ -1,14 +1,28 @@
|
|||||||
# https://www.playframework.com/documentation/latest/Configuration
|
# https://www.playframework.com/documentation/latest/Configuration
|
||||||
play.filters.disabled += play.filters.csrf.CSRFFilter
|
play.filters.disabled += play.filters.csrf.CSRFFilter
|
||||||
|
play.filters.disabled += play.filters.hosts.AllowedHostsFilter
|
||||||
|
|
||||||
|
play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n"
|
||||||
|
play.http.secret.key=${?APPLICATION_SECRET}
|
||||||
|
|
||||||
auth {
|
auth {
|
||||||
issuer = "knockoutwhistweb"
|
issuer = "knockoutwhistweb"
|
||||||
audience = "ui"
|
audience = "ui"
|
||||||
# ${?PUBLIC_KEY_FILE}
|
privateKeyFile = ${?PRIVATE_KEY_FILE}
|
||||||
privateKeyFile = "D:\\Workspaces\\Gitops\\rsa512-private.pem"
|
privateKeyPem = ${?PRIVATE_KEY_PEM}
|
||||||
privateKeyPem = ${?PUBLIC_KEY_PEM}
|
publicKeyFile = ${?PUBLIC_KEY_FILE}
|
||||||
#${?PUBLIC_KEY_FILE}
|
|
||||||
publicKeyFile = "D:\\Workspaces\\Gitops\\rsa512-public.pem"
|
|
||||||
publicKeyPem = ${?PUBLIC_KEY_PEM}
|
publicKeyPem = ${?PUBLIC_KEY_PEM}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
play.http.context="/api"
|
||||||
|
|
||||||
|
play.modules.enabled += "modules.GatewayModule"
|
||||||
|
|
||||||
|
play.filters.enabled += "play.filters.cors.CORSFilter"
|
||||||
|
|
||||||
|
play.filters.cors {
|
||||||
|
allowedOrigins = ["http://localhost:5173", "http://localhost:3000", "http://localhost:8081"]
|
||||||
|
allowedCredentials = true
|
||||||
|
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||||
|
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,16 +5,17 @@
|
|||||||
<!DOCTYPE configuration>
|
<!DOCTYPE configuration>
|
||||||
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
|
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder" />
|
||||||
<import class="ch.qos.logback.classic.AsyncAppender"/>
|
<import class="ch.qos.logback.classic.AsyncAppender" />
|
||||||
<import class="ch.qos.logback.core.FileAppender"/>
|
<import class="ch.qos.logback.core.FileAppender" />
|
||||||
<import class="ch.qos.logback.core.ConsoleAppender"/>
|
<import class="ch.qos.logback.core.ConsoleAppender" />
|
||||||
|
|
||||||
<appender name="FILE" class="FileAppender">
|
<appender name="FILE" class="FileAppender">
|
||||||
<file>${application.home:-.}/logs/application.log</file>
|
<file>${application.home:-.}/logs/application.log</file>
|
||||||
<encoder class="PatternLayoutEncoder">
|
<encoder class="PatternLayoutEncoder">
|
||||||
<charset>UTF-8</charset>
|
<charset>UTF-8</charset>
|
||||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
|
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n
|
||||||
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
@@ -27,24 +28,25 @@
|
|||||||
<!-- <withJansi>true</withJansi> -->
|
<!-- <withJansi>true</withJansi> -->
|
||||||
<encoder class="PatternLayoutEncoder">
|
<encoder class="PatternLayoutEncoder">
|
||||||
<charset>UTF-8</charset>
|
<charset>UTF-8</charset>
|
||||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
|
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n
|
||||||
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="ASYNCFILE" class="AsyncAppender">
|
<appender name="ASYNCFILE" class="AsyncAppender">
|
||||||
<appender-ref ref="FILE"/>
|
<appender-ref ref="FILE" />
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="ASYNCSTDOUT" class="AsyncAppender">
|
<appender name="ASYNCSTDOUT" class="AsyncAppender">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT" />
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="play" level="INFO"/>
|
<logger name="play" level="INFO" />
|
||||||
<logger name="application" level="DEBUG"/>
|
<logger name="application" level="DEBUG" />
|
||||||
|
|
||||||
<root level="WARN">
|
<root level="WARN">
|
||||||
<appender-ref ref="ASYNCFILE"/>
|
<appender-ref ref="ASYNCFILE" />
|
||||||
<appender-ref ref="ASYNCSTDOUT"/>
|
<appender-ref ref="ASYNCSTDOUT" />
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
# https://www.playframework.com/documentation/latest/ScalaRouting
|
# https://www.playframework.com/documentation/latest/ScalaRouting
|
||||||
# ~~~~
|
# ~~~~
|
||||||
|
|
||||||
|
# For the javascript routing
|
||||||
|
GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
|
||||||
# Primary routes
|
# Primary routes
|
||||||
GET / controllers.MainMenuController.index()
|
GET / controllers.MainMenuController.index()
|
||||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||||
@@ -11,19 +12,22 @@ GET /assets/*file controllers.Assets.versioned(path="/public",
|
|||||||
# Main menu routes
|
# Main menu routes
|
||||||
GET /mainmenu controllers.MainMenuController.mainMenu()
|
GET /mainmenu controllers.MainMenuController.mainMenu()
|
||||||
GET /rules controllers.MainMenuController.rules()
|
GET /rules controllers.MainMenuController.rules()
|
||||||
|
GET /navSPA/:pType controllers.MainMenuController.navSPA(pType)
|
||||||
|
|
||||||
POST /createGame controllers.MainMenuController.createGame()
|
POST /createGame controllers.MainMenuController.createGame()
|
||||||
POST /joinGame controllers.MainMenuController.joinGame()
|
POST /joinGame controllers.MainMenuController.joinGame()
|
||||||
|
|
||||||
# User authentication routes
|
# User authentication routes
|
||||||
GET /login controllers.UserController.login()
|
|
||||||
POST /login controllers.UserController.login_Post()
|
POST /login controllers.UserController.login_Post()
|
||||||
|
POST /logout controllers.UserController.logoutPost()
|
||||||
GET /logout controllers.UserController.logout()
|
GET /userInfo controllers.UserController.getUserInfo()
|
||||||
|
|
||||||
# In-game routes
|
# In-game routes
|
||||||
GET /game/:id controllers.IngameController.game(id: String)
|
GET /game/:id controllers.IngameController.game(id: String)
|
||||||
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
|
||||||
POST /game/:id/start controllers.IngameController.startGame(id: String)
|
|
||||||
|
|
||||||
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
# Websocket
|
||||||
|
GET /websocket controllers.WebsocketController.socket()
|
||||||
|
|
||||||
|
# Status
|
||||||
|
GET /status controllers.StatusController.requestStatus()
|
||||||
|
GET /status/:gameId controllers.StatusController.game(gameId: String)
|
||||||
BIN
knockoutwhistweb/public/images/logo.png
Normal file
BIN
knockoutwhistweb/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
653
knockoutwhistweb/public/javascripts/events.js
Normal file
653
knockoutwhistweb/public/javascripts/events.js
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
var canPlayCard = false;
|
||||||
|
const PlayerHandComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hand: [],
|
||||||
|
isDogPhase: false,
|
||||||
|
isAwaitingResponse: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isHandInactive() {
|
||||||
|
//TODO: Needs implementation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="row justify-content-center g-2 mt-4 bottom-div"
|
||||||
|
style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
|
||||||
|
|
||||||
|
<div id="card-slide" class="row justify-content-center ingame-cards-slide" :class="{'inactive': isHandInactive }">
|
||||||
|
|
||||||
|
<div v-for="card in hand" :key="card.idx" class="col-auto handcard" style="border-radius: 6px">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none"
|
||||||
|
:data-card-id="card.idx"
|
||||||
|
style="border-radius: 6px"
|
||||||
|
@click="handlePlayCard(card.idx)">
|
||||||
|
|
||||||
|
<img :src="getCardImagePath(card.card)" width="120px" style="border-radius: 6px" :alt="card.card"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDogPhase" class="mt-2">
|
||||||
|
<button class="btn btn-danger" @click="handleSkipDogLife()">Skip Turn</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
updateHand(eventData) {
|
||||||
|
this.hand = eventData.hand.map(card => ({
|
||||||
|
idx: parseInt(card.idx, 10),
|
||||||
|
card: card.card
|
||||||
|
}));
|
||||||
|
this.isDogPhase = false;
|
||||||
|
|
||||||
|
console.log("Vue Data Updated. Hand size:", this.hand.length);
|
||||||
|
|
||||||
|
if (this.hand.length > 0) {
|
||||||
|
console.log("First card path check:", this.getCardImagePath(this.hand[0].card));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlePlayCard(cardidx) {
|
||||||
|
if(this.isAwaitingResponse) return
|
||||||
|
if(!canPlayCard) return
|
||||||
|
canPlayCard = false;
|
||||||
|
this.isAwaitingResponse = true
|
||||||
|
|
||||||
|
|
||||||
|
console.debug(`Playing card ${cardidx} from hand`)
|
||||||
|
|
||||||
|
const wiggleKeyframes = [
|
||||||
|
{ transform: 'translateX(0)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(5px)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(0)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const wiggleTiming = {
|
||||||
|
duration: 400,
|
||||||
|
iterations: 1,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
fill: 'forwards'
|
||||||
|
};
|
||||||
|
const targetButton = this.$el.querySelector(`[data-card-id="${cardidx}"]`);
|
||||||
|
const cardElement = targetButton ? targetButton.closest('.handcard') : null;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
cardindex: cardidx.toString(),
|
||||||
|
isDog: false
|
||||||
|
}
|
||||||
|
sendEventAndWait("PlayCard", payload).then(
|
||||||
|
() => {
|
||||||
|
this.hand = this.hand.filter(card => card.idx !== cardidx);
|
||||||
|
|
||||||
|
this.hand.forEach((card, index) => {
|
||||||
|
card.idx = index;
|
||||||
|
})
|
||||||
|
this.isAwaitingResponse = false;
|
||||||
|
}
|
||||||
|
).catch(
|
||||||
|
(err) => {
|
||||||
|
if (cardElement) {
|
||||||
|
cardElement.animate(wiggleKeyframes, wiggleTiming);
|
||||||
|
} else {
|
||||||
|
console.warn(`Could not find DOM element for card index ${cardidx} to wiggle.`);
|
||||||
|
}
|
||||||
|
this.isAwaitingResponse = false;
|
||||||
|
canPlayCard = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
handleSkipDogLife() {
|
||||||
|
globalThis.handleSkipDogLife();
|
||||||
|
},
|
||||||
|
getCardImagePath(cardName) {
|
||||||
|
return `/assets/images/cards/${cardName}.png`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ScoreBoardComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
trumpsuit: 'N/A',
|
||||||
|
playerScores: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="score-table mt-5" id="score-table-container">
|
||||||
|
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between score-header pb-1">
|
||||||
|
<div style="width: 50%">PLAYER</div>
|
||||||
|
<div style="width: 50%">TRICKS</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="score-table-body">
|
||||||
|
<div v-for="(player, index) in playerScores"
|
||||||
|
:key="player.name"
|
||||||
|
class="d-flex justify-content-between score-row pt-1">
|
||||||
|
|
||||||
|
<div style="width: 50%" class="text-truncate">
|
||||||
|
{{ player.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width: 50%">
|
||||||
|
{{ player.tricks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
calculateNewScores(players, tricklist) {
|
||||||
|
const playercounts = new Map();
|
||||||
|
players.forEach(player => {
|
||||||
|
playercounts.set(player, 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
tricklist.forEach(playerWonTrick => {
|
||||||
|
if (playerWonTrick !== "Trick in Progress" && playercounts.has(playerWonTrick)) {
|
||||||
|
playercounts.set(playerWonTrick, playercounts.get(playerWonTrick) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newScores = players.map(name => ({
|
||||||
|
name: name,
|
||||||
|
tricks: playercounts.get(name) || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
newScores.sort((a, b) => b.tricks - a.tricks);
|
||||||
|
|
||||||
|
return newScores;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNewRoundData(eventData) {
|
||||||
|
console.log("Vue Scoreboard Data Update Triggered: New Round!");
|
||||||
|
|
||||||
|
this.playerScores = eventData.players.map(player => ({
|
||||||
|
name: player,
|
||||||
|
tricks: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTrickEndData(eventData) {
|
||||||
|
const { playerwon, playersin, tricklist } = eventData;
|
||||||
|
|
||||||
|
console.log(`Vue Scoreboard Data Update Triggered: ${playerwon} won the trick!`);
|
||||||
|
|
||||||
|
this.playerScores = this.calculateNewScores(playersin, tricklist);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const GameInfoComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
trumpsuit: 'No Trumpsuit',
|
||||||
|
firstCardImagePath: '/assets/images/cards/1B.png',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-semibold mb-1">Trumpsuit</h4>
|
||||||
|
<p class="fs-5 text-primary" id="trumpsuit">{{ trumpsuit }}</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h5 class="fw-semibold mt-4 mb-1">First Card</h5>
|
||||||
|
<div class="d-inline-block border rounded shadow-sm p-1 bg-light" id="first-card-container">
|
||||||
|
|
||||||
|
<img :src="firstCardImagePath" alt="First Card" width="80px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
resetFirstCard(eventData) {
|
||||||
|
console.log("GameInfoComponent: Resetting First Card to placeholder.");
|
||||||
|
this.firstCardImagePath = '/assets/images/cards/1B.png';
|
||||||
|
},
|
||||||
|
updateFirstCard(eventData) {
|
||||||
|
const firstCardId = eventData.firstCard;
|
||||||
|
console.log("GameInfoComponent: Updating First Card to:", firstCardId);
|
||||||
|
|
||||||
|
let imageSource;
|
||||||
|
if (firstCardId === "BLANK" || !firstCardId) {
|
||||||
|
imageSource = "/assets/images/cards/1B.png";
|
||||||
|
} else {
|
||||||
|
imageSource = `/assets/images/cards/${firstCardId}.png`;
|
||||||
|
}
|
||||||
|
this.firstCardImagePath = imageSource;
|
||||||
|
},
|
||||||
|
updateTrumpsuit(eventData) {
|
||||||
|
this.trumpsuit = eventData.trumpsuit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const TrickDisplayComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playedCards: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div class="d-flex justify-content-center g-3" id="trick-cards-content">
|
||||||
|
<div v-for="(play, index) in playedCards" :key="index" class="col-auto">
|
||||||
|
<div class="card text-center shadow-sm border-0 bg-transparent" style="width: 7rem;
|
||||||
|
backdrop-filter: blur(4px);">
|
||||||
|
<div class="p-2">
|
||||||
|
<img :src="getCardImagePath(play.cardId)" width="100%" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 bg-transparent">
|
||||||
|
<small class="fw-semibold text-secondary">{{ play.player }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getCardImagePath(cardId) {
|
||||||
|
return `/assets/images/cards/${cardId}.png`;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPlayedCards() {
|
||||||
|
console.log("TrickDisplayComponent: Clearing played cards.");
|
||||||
|
this.playedCards = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePlayedCards(eventData) {
|
||||||
|
console.log("TrickDisplayComponent: Updating played cards.");
|
||||||
|
|
||||||
|
this.playedCards = eventData.playedCards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function formatPlayerName(player) {
|
||||||
|
let name = player.name;
|
||||||
|
if (player.dog) {
|
||||||
|
name += " 🐶";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TurnComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPlayerName: 'Waiting...',
|
||||||
|
nextPlayers: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div class="turn-tracker-container">
|
||||||
|
<h4 class="fw-semibold mb-1">Current Player</h4>
|
||||||
|
<p class="fs-3 fw-bold text-success" id="current-player-name">{{ currentPlayerName }}</p>
|
||||||
|
|
||||||
|
<div v-if="nextPlayers.length > 0">
|
||||||
|
<h5 class="fw-semibold mt-4 mb-1" id="next-players-text">Next Players</h5>
|
||||||
|
<div id="next-players-container">
|
||||||
|
<p v-for="name in nextPlayers" :key="name" class="fs-5 text-primary">{{ name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
updateTurnData(eventData) {
|
||||||
|
console.log("TurnComponent: Updating turn data.");
|
||||||
|
const { currentPlayer, nextPlayers } = eventData;
|
||||||
|
|
||||||
|
this.currentPlayerName = formatPlayerName(currentPlayer);
|
||||||
|
|
||||||
|
this.nextPlayers = nextPlayers.map(player => formatPlayerName(player));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const LobbyComponent = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lobbyName: 'Loading...',
|
||||||
|
lobbyId: 'default',
|
||||||
|
isHost: false,
|
||||||
|
maxPlayers: 0,
|
||||||
|
players: [],
|
||||||
|
showKickedModal: false,
|
||||||
|
kickedEventData: null,
|
||||||
|
showSessionClosedModal: false,
|
||||||
|
sessionClosedEventData: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<main class="lobby-background vh-100" id="lobbybackground">
|
||||||
|
|
||||||
|
<div v-if="showKickedModal" class="modal fade show d-block"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="kickedModalTitle"
|
||||||
|
aria-modal="true"
|
||||||
|
style="background-color: rgba(0,0,0,0.5);">
|
||||||
|
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="kickedModalTitle">Kicked</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>You've been kicked from the lobby.</p>
|
||||||
|
<p class="text-muted small">You'll get redirected to the mainmenu in 5 seconds...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="showSessionClosedModal" class="modal fade show d-block"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="sessionClosedModalTitle"
|
||||||
|
aria-modal="true"
|
||||||
|
style="background-color: rgba(0,0,0,0.5);">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="sessionClosedModalTitle">Session Closed</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>The session was closed.</p>
|
||||||
|
<p class="text-muted small">You'll get redirected to the mainmenu in 5 seconds...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="p-3 fs-1 d-flex align-items-center">
|
||||||
|
<div class="text-center" style="flex-grow: 1;">
|
||||||
|
Lobby-Name: {{ lobbyName }}
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-danger ms-auto" @click="leaveGame(lobbyId)">Exit</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="p-3 text-center fs-4" id="playerAmount">
|
||||||
|
Players: {{ players.length }} / {{ maxPlayers }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center align-items-center flex-grow-1">
|
||||||
|
|
||||||
|
<template v-if="isHost">
|
||||||
|
<div id="players" class="justify-content-center align-items-center d-flex flex-wrap">
|
||||||
|
<div v-for="player in players" :key="player.id" class="col-auto my-auto m-3">
|
||||||
|
<div class="card" style="width: 18rem;">
|
||||||
|
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{{ player.name }} <span v-if="player.self">(You)</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<template v-if="player.self">
|
||||||
|
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="btn btn-danger" @click="handleKickPlayer(player.id)">Remove</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 text-center mb-5">
|
||||||
|
<div class="btn btn-success" @click="startGame()">Start Game</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div id="players" class="justify-content-center align-items-center d-flex flex-wrap">
|
||||||
|
<div v-for="player in players" :key="player.id" class="col-auto my-auto m-3">
|
||||||
|
<div class="card" style="width: 18rem;">
|
||||||
|
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{{ player.name }} <span v-if="player.self">(You)</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 text-center mt-3">
|
||||||
|
<p class="fs-4">Waiting for the host to start the game...</p>
|
||||||
|
<div class="spinner-border mt-1" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateLobbyData(eventData) {
|
||||||
|
console.log("LobbyComponent: Received Lobby Update Event.");
|
||||||
|
|
||||||
|
this.isHost = eventData.host;
|
||||||
|
this.maxPlayers = eventData.maxPlayers;
|
||||||
|
this.players = eventData.players;
|
||||||
|
},
|
||||||
|
|
||||||
|
setInitialData(name, id) {
|
||||||
|
this.lobbyName = name;
|
||||||
|
this.lobbyId = id;
|
||||||
|
},
|
||||||
|
startGame() {
|
||||||
|
globalThis.startGame()
|
||||||
|
},
|
||||||
|
leaveGame(gameId) {
|
||||||
|
//TODO: Needs implementation
|
||||||
|
},
|
||||||
|
handleKickPlayer(playerId) {
|
||||||
|
globalThis.handleKickPlayer(playerId)
|
||||||
|
},
|
||||||
|
showKickModal(eventData) {
|
||||||
|
this.showKickedModal = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.kickedEventData = eventData;
|
||||||
|
this.showKickedModal = false;
|
||||||
|
|
||||||
|
if (typeof globalThis.receiveGameStateChange === 'function') {
|
||||||
|
globalThis.receiveGameStateChange(this.kickedEventData);
|
||||||
|
} else {
|
||||||
|
console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
showSessionClosedModal(eventData) {
|
||||||
|
this.sessionClosedEventData = eventData;
|
||||||
|
this.showSessionClosedModal = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showSessionClosedModal = false;
|
||||||
|
|
||||||
|
if (typeof globalThis.receiveGameStateChange === 'function') {
|
||||||
|
globalThis.receiveGameStateChange(this.sessionClosedEventData);
|
||||||
|
} else {
|
||||||
|
console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function requestCardEvent(eventData) {
|
||||||
|
//TODO: Needs correct implementation of setting the inactive class in the PlayerHandComponent
|
||||||
|
}
|
||||||
|
function receiveGameStateChange(eventData) {
|
||||||
|
const content = eventData.content;
|
||||||
|
const title = eventData.title || 'Knockout Whist';
|
||||||
|
const url = eventData.url || null;
|
||||||
|
|
||||||
|
exchangeBody(content, title, url);
|
||||||
|
}
|
||||||
|
function receiveRoundEndEvent(eventData) {
|
||||||
|
//TODO: When alert is working, set an alert that shows how won the round and with how much tricks.
|
||||||
|
}
|
||||||
|
let playerHandApp = null;
|
||||||
|
let scoreBoardApp = null;
|
||||||
|
let gameInfoApp = null;
|
||||||
|
let trickDisplayApp = null;
|
||||||
|
let turnApp = null;
|
||||||
|
globalThis.initGameVueComponents = function() {
|
||||||
|
// Initializing PlayerHandComponent
|
||||||
|
const app = Vue.createApp(PlayerHandComponent);
|
||||||
|
|
||||||
|
playerHandApp = app;
|
||||||
|
const mountedHand = app.mount('#player-hand-container');
|
||||||
|
|
||||||
|
if (mountedHand && mountedHand.updateHand) {
|
||||||
|
globalThis.updatePlayerHand = mountedHand.updateHand;
|
||||||
|
onEvent("ReceivedHandEvent", globalThis.updatePlayerHand);
|
||||||
|
console.log("PLAYER HAND SYSTEM: updatePlayerHand successfully exposed.");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: PlayerHandComponent mount failed. Check if #player-hand-container exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing Scoreboard
|
||||||
|
if (scoreBoardApp) return
|
||||||
|
|
||||||
|
const app2 = Vue.createApp(ScoreBoardComponent)
|
||||||
|
scoreBoardApp = app2
|
||||||
|
const mountedHand2 = app2.mount('#score-table')
|
||||||
|
if (mountedHand2) {
|
||||||
|
globalThis.updateNewRoundData = mountedHand2.updateNewRoundData;
|
||||||
|
onEvent("NewRoundEvent", handleNewRoundEvent);
|
||||||
|
|
||||||
|
globalThis.updateTrickEndData = mountedHand2.updateTrickEndData;
|
||||||
|
onEvent("TrickEndEvent", globalThis.updateTrickEndData);
|
||||||
|
console.log("SCOREBOARD: updateNewRoundData successfully exposed.");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: Scoreboard mount failed. Check if #score-table exists.");
|
||||||
|
}
|
||||||
|
// Initializing Gameinfo
|
||||||
|
if (gameInfoApp) return
|
||||||
|
|
||||||
|
const app3 = Vue.createApp(GameInfoComponent)
|
||||||
|
gameInfoApp = app3
|
||||||
|
const mountedGameInfo = app3.mount('#game-info-component')
|
||||||
|
if(mountedGameInfo) {
|
||||||
|
globalThis.resetFirstCard = mountedGameInfo.resetFirstCard;
|
||||||
|
globalThis.updateFirstCard = mountedGameInfo.updateFirstCard;
|
||||||
|
globalThis.updateTrumpsuit = mountedGameInfo.updateTrumpsuit
|
||||||
|
onEvent("NewTrickEvent", handleNewTrickEvent);
|
||||||
|
console.log("GameInfo: resetFirstCard successfully exposed.");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: GameInfo mount failed. Check if #score-table exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing TrickCardContainer
|
||||||
|
if (trickDisplayApp) return;
|
||||||
|
const app4 = Vue.createApp(TrickDisplayComponent);
|
||||||
|
trickDisplayApp = app4;
|
||||||
|
const mountedTrickDisplay = app4.mount('#trick-cards-container');
|
||||||
|
|
||||||
|
if (mountedTrickDisplay) {
|
||||||
|
globalThis.clearPlayedCards = mountedTrickDisplay.clearPlayedCards;
|
||||||
|
globalThis.updatePlayedCards = mountedTrickDisplay.updatePlayedCards;
|
||||||
|
onEvent("CardPlayedEvent", handleCardPlayedEvent)
|
||||||
|
console.log("TRICK DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: TrickDisplay mount failed. Check if #trick-cards-container exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing TurnContainer
|
||||||
|
if (turnApp) return;
|
||||||
|
const app5 = Vue.createApp(TurnComponent)
|
||||||
|
turnApp = app5;
|
||||||
|
const mountedTurnApp = app5.mount('#turn-component')
|
||||||
|
|
||||||
|
if(mountedTurnApp) {
|
||||||
|
globalThis.updateTurnData = mountedTurnApp.updateTurnData;
|
||||||
|
onEvent("TurnEvent", globalThis.updateTurnData);
|
||||||
|
console.log("TURN DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: TURNAPP mount failed. Check if #trick-cards-container exists.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let lobbyApp = null;
|
||||||
|
globalThis.initLobbyVueComponents = function(initialLobbyName, initialLobbyId, initialIsHost, initialMaxPlayers, initialPlayers) {
|
||||||
|
|
||||||
|
if (lobbyApp) return;
|
||||||
|
|
||||||
|
const appLobby = Vue.createApp(LobbyComponent);
|
||||||
|
lobbyApp = appLobby;
|
||||||
|
const mountedLobby = appLobby.mount('#lobby-app-mount');
|
||||||
|
|
||||||
|
if (mountedLobby) {
|
||||||
|
mountedLobby.setInitialData(initialLobbyName, initialLobbyId);
|
||||||
|
|
||||||
|
//Damit beim erstmaligen Betreten der Lobby die Spieler etc. angezeigt werden.
|
||||||
|
mountedLobby.updateLobbyData({
|
||||||
|
host: initialIsHost,
|
||||||
|
maxPlayers: initialMaxPlayers,
|
||||||
|
players: initialPlayers
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.updateLobbyData = mountedLobby.updateLobbyData;
|
||||||
|
globalThis.showKickModal = mountedLobby.showKickModal;
|
||||||
|
globalThis.showSessionClosedModal = mountedLobby.showSessionClosedModal;
|
||||||
|
onEvent("LobbyUpdateEvent", globalThis.updateLobbyData);
|
||||||
|
onEvent("KickEvent", globalThis.showKickModal);
|
||||||
|
onEvent("SessionClosed", globalThis.showSessionClosedModal);
|
||||||
|
console.log("LobbyComponent successfully mounted and registered events.");
|
||||||
|
} else {
|
||||||
|
console.error("FATAL ERROR: LobbyComponent mount failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleCardPlayedEvent(eventData) {
|
||||||
|
console.log("CardPlayedEvent received. Updating Game Info and Trick Display.");
|
||||||
|
|
||||||
|
if (typeof globalThis.updateFirstCard === 'function') {
|
||||||
|
globalThis.updateFirstCard(eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof globalThis.updatePlayedCards === 'function') {
|
||||||
|
globalThis.updatePlayedCards(eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleNewTrickEvent(eventData) {
|
||||||
|
if (typeof globalThis.resetFirstCard === 'function') {
|
||||||
|
globalThis.resetFirstCard(eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof globalThis.clearPlayedCards === 'function') {
|
||||||
|
globalThis.clearPlayedCards();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleNewRoundEvent(eventData) {
|
||||||
|
if (typeof globalThis.updateNewRoundData === 'function') {
|
||||||
|
globalThis.updateNewRoundData(eventData);
|
||||||
|
}
|
||||||
|
if (typeof globalThis.updateTrumpsuit === 'function') {
|
||||||
|
globalThis.updateTrumpsuit(eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
||||||
|
onEvent("LeftEvent", receiveGameStateChange)
|
||||||
|
onEvent("RequestCardEvent", requestCardEvent)
|
||||||
|
onEvent("RoundEndEvent", receiveRoundEndEvent)
|
||||||
33
knockoutwhistweb/public/javascripts/interact.js
Normal file
33
knockoutwhistweb/public/javascripts/interact.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
function handlePlayCard(cardidx) {
|
||||||
|
//TODO: Needs implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSkipDogLife(button) {
|
||||||
|
// TODO needs implementation
|
||||||
|
}
|
||||||
|
function startGame() {
|
||||||
|
sendEvent("StartGame")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTrumpSelection(object) {
|
||||||
|
const $button = $(object);
|
||||||
|
const trumpIndex = parseInt($button.data('trump'));
|
||||||
|
const payload = {
|
||||||
|
suitIndex: trumpIndex
|
||||||
|
}
|
||||||
|
sendEvent("PickTrumpsuit", payload)
|
||||||
|
|
||||||
|
}
|
||||||
|
function handleKickPlayer(playerId) {
|
||||||
|
sendEvent("KickPlayer", {
|
||||||
|
playerId: playerId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function handleReturnToLobby() {
|
||||||
|
sendEvent("ReturnToLobby")
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.startGame = startGame
|
||||||
|
globalThis.handleTrumpSelection = handleTrumpSelection
|
||||||
|
globalThis.handleKickPlayer = handleKickPlayer
|
||||||
|
globalThis.handleReturnToLobby = handleReturnToLobby
|
||||||
@@ -1,3 +1,222 @@
|
|||||||
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
/*!
|
||||||
console.log('callback - particles.js config loaded');
|
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||||
});
|
* Copyright 2011-2025 The Bootstrap Authors
|
||||||
|
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const getStoredTheme = () => localStorage.getItem('theme')
|
||||||
|
const setStoredTheme = theme => localStorage.setItem('theme', theme)
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = theme => {
|
||||||
|
if (theme === 'auto') {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
const showActiveTheme = (theme, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector('#bd-theme')
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector('#bd-theme-text')
|
||||||
|
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
||||||
|
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
||||||
|
element.classList.remove('active')
|
||||||
|
element.setAttribute('aria-pressed', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
btnToActive.classList.add('active')
|
||||||
|
btnToActive.setAttribute('aria-pressed', 'true')
|
||||||
|
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
|
||||||
|
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
showActiveTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]')
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const theme = toggle.getAttribute('data-bs-theme-value')
|
||||||
|
setStoredTheme(theme)
|
||||||
|
setTheme(theme)
|
||||||
|
showActiveTheme(theme, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
function createGameJS() {
|
||||||
|
let lobbyName = $('#lobbyname').val();
|
||||||
|
if ($.trim(lobbyName) === "") {
|
||||||
|
lobbyName = "DefaultLobby"
|
||||||
|
}
|
||||||
|
const jsonObj = {
|
||||||
|
lobbyname: lobbyName,
|
||||||
|
playeramount: $("#playeramount").val()
|
||||||
|
}
|
||||||
|
sendGameCreationRequest(jsonObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendGameCreationRequest(dataObject) {
|
||||||
|
const route = jsRoutes.controllers.MainMenuController.createGame();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: route.url,
|
||||||
|
type: route.type,
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(dataObject),
|
||||||
|
dataType: 'json',
|
||||||
|
success: (data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
error: ((jqXHR) => {
|
||||||
|
const errorData = JSON.parse(jqXHR.responseText);
|
||||||
|
if (errorData && errorData.errorMessage) {
|
||||||
|
alert(`${errorData.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exchangeBody(content, title = "Knockout Whist", url = null) {
|
||||||
|
if (url) {
|
||||||
|
window.history.pushState({}, title, url);
|
||||||
|
}
|
||||||
|
$("#main-body").html(content);
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
const username = $('#username').val();
|
||||||
|
const password = $('#password').val();
|
||||||
|
|
||||||
|
const jsonObj = {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = jsRoutes.controllers.UserController.login_Post();
|
||||||
|
$.ajax({
|
||||||
|
url: route.url,
|
||||||
|
type: route.type,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(jsonObj),
|
||||||
|
success: (data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
exchangeBody(data.content, 'Knockout Whist - Create Game', data.redirectUrl);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert('Login failed. Please check your credentials and try again.');
|
||||||
|
}),
|
||||||
|
error: ((jqXHR) => {
|
||||||
|
const errorData = JSON.parse(jqXHR.responseText);
|
||||||
|
if (errorData?.errorMessage) {
|
||||||
|
alert(`${errorData.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinGame() {
|
||||||
|
const gameId = $('#gameId').val();
|
||||||
|
|
||||||
|
const jsonObj = {
|
||||||
|
gameId: gameId
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = jsRoutes.controllers.MainMenuController.joinGame();
|
||||||
|
$.ajax({
|
||||||
|
url: route.url,
|
||||||
|
type: route.type,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(jsonObj),
|
||||||
|
success: (data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert('Could not join the game. Please check the Game ID and try again.');
|
||||||
|
}),
|
||||||
|
error: ((jqXHR) => {
|
||||||
|
const errorData = JSON.parse(jqXHR.responseText);
|
||||||
|
if (errorData?.errorMessage) {
|
||||||
|
alert(`${errorData.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function navSpa(page, title) {
|
||||||
|
const route = jsRoutes.controllers.MainMenuController.navSPA(page);
|
||||||
|
$.ajax({
|
||||||
|
url: route.url,
|
||||||
|
type: route.type,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(jsonObj),
|
||||||
|
success: (data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
exchangeBody(data.content, title, data.redirectUrl);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert('Could not join the game. Please check the Game ID and try again.');
|
||||||
|
}),
|
||||||
|
error: ((jqXHR) => {
|
||||||
|
const errorData = JSON.parse(jqXHR.responseText);
|
||||||
|
if (errorData?.errorMessage) {
|
||||||
|
alert(`${errorData.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
globalThis.exchangeBody = exchangeBody;
|
||||||
File diff suppressed because it is too large
Load Diff
192
knockoutwhistweb/public/javascripts/websocket.js
Normal file
192
knockoutwhistweb/public/javascripts/websocket.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
let ws = null;
|
||||||
|
const pending = new Map();
|
||||||
|
const handlers = new Map();
|
||||||
|
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
function setupSocketHandlers(socket) {
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
console.debug("SERVER MESSAGE:", event.data);
|
||||||
|
let msg;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(event.data);
|
||||||
|
} catch (e) {
|
||||||
|
console.debug("Non-JSON message from server:", event.data, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = msg.id;
|
||||||
|
const eventType = msg.event;
|
||||||
|
const status = msg.status;
|
||||||
|
const data = msg.data;
|
||||||
|
|
||||||
|
if (id && typeof status === "string") {
|
||||||
|
const entry = pending.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
clearTimeout(entry.timer);
|
||||||
|
pending.delete(id);
|
||||||
|
|
||||||
|
if (status === "success") {
|
||||||
|
entry.resolve(data === undefined ? {} : data);
|
||||||
|
} else {
|
||||||
|
entry.reject(new Error(msg.error || "Server returned error"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id && eventType) {
|
||||||
|
const handler = handlers.get(eventType);
|
||||||
|
const sendResponse = (result) => {
|
||||||
|
const response = {id: id, event: eventType, status: result};
|
||||||
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
|
socket.send(JSON.stringify(response));
|
||||||
|
} else {
|
||||||
|
console.warn("Cannot send response, websocket not open");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
console.warn("No handler for event:", eventType);
|
||||||
|
sendResponse({error: "No handler for event: " + eventType});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Promise.resolve(handler(data === undefined ? {} : data))
|
||||||
|
.then(_ => sendResponse("success"))
|
||||||
|
.catch(_ => sendResponse("error"));
|
||||||
|
} catch (err) {
|
||||||
|
sendResponse("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
console.error("WebSocket Error:", error);
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
for (const [id, entry] of pending.entries()) {
|
||||||
|
clearTimeout(entry.timer);
|
||||||
|
entry.reject(new Error("WebSocket error/closed"));
|
||||||
|
pending.delete(id);
|
||||||
|
}
|
||||||
|
if (socket.readyState === WebSocket.OPEN) socket.close(1000, "Unexpected error.");
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = (event) => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
for (const [id, entry] of pending.entries()) {
|
||||||
|
clearTimeout(entry.timer);
|
||||||
|
entry.reject(new Error("WebSocket closed"));
|
||||||
|
pending.delete(id);
|
||||||
|
}
|
||||||
|
if (event.wasClean) {
|
||||||
|
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
||||||
|
} else {
|
||||||
|
console.warn('Connection died unexpectedly.');
|
||||||
|
}
|
||||||
|
location.href = "/mainmenu";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket(url = null) {
|
||||||
|
if (!url) {
|
||||||
|
const loc = window.location;
|
||||||
|
const protocol = loc.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
url = protocol + "//" + loc.host + "/websocket";
|
||||||
|
}
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
||||||
|
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const prevOnOpen = ws.onopen;
|
||||||
|
const prevOnError = ws.onerror;
|
||||||
|
ws.onopen = (ev) => {
|
||||||
|
if (prevOnOpen) prevOnOpen(ev);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
if (prevOnError) prevOnError(err);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
setupSocketHandlers(ws);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log("WebSocket connection established!");
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
sendEventAndWait("ping", {}).then(
|
||||||
|
() => console.debug("PING RESPONSE RECEIVED"),
|
||||||
|
).catch(
|
||||||
|
(err) => console.warn("PING ERROR:", err.message),
|
||||||
|
);
|
||||||
|
console.debug("PING SENT");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectWebSocket(code = 1000, reason = "Client disconnect") {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
if (ws) {
|
||||||
|
try {
|
||||||
|
ws.close(code, reason);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendEvent(eventType, eventData) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn("WebSocket is not open. Unable to send message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||||
|
const message = {id: id, event: eventType, data: eventData};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
console.debug("SENT:", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
return Promise.reject(new Error("WebSocket is not open"));
|
||||||
|
}
|
||||||
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||||
|
const message = {id: id, event: eventType, data: eventData};
|
||||||
|
const p = new Promise((resolve, reject) => {
|
||||||
|
const timerId = setTimeout(() => {
|
||||||
|
if (pending.has(id)) {
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`No response within ${timeoutMs}ms for id=${id}`));
|
||||||
|
}
|
||||||
|
}, timeoutMs);
|
||||||
|
pending.set(id, {resolve, reject, timer: timerId});
|
||||||
|
});
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
console.debug("SENT (await):", message);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEvent(eventType, handler) {
|
||||||
|
handlers.set(eventType, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.sendEvent = sendEvent;
|
||||||
|
globalThis.sendEventAndWait = sendEventAndWait;
|
||||||
|
globalThis.onEvent = onEvent;
|
||||||
|
globalThis.connectWebSocket = connectWebSocket;
|
||||||
|
globalThis.disconnectWebSocket = disconnectWebSocket;
|
||||||
|
globalThis.isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;
|
||||||
@@ -13,33 +13,33 @@ import play.api.test.Helpers.*
|
|||||||
*/
|
*/
|
||||||
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
|
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
|
||||||
|
|
||||||
// "HomeController GET" should {
|
// "HomeController GET" should {
|
||||||
//
|
//
|
||||||
// "render the index page from a new instance of controller" in {
|
// "render the index page from a new instance of controller" in {
|
||||||
// val controller = new HomeController(stubControllerComponents())
|
// val controller = new HomeController(stubControllerComponents())
|
||||||
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||||
//
|
//
|
||||||
// status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
// contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
// contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// "render the index page from the application" in {
|
// "render the index page from the application" in {
|
||||||
// val controller = inject[HomeController]
|
// val controller = inject[HomeController]
|
||||||
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||||
//
|
//
|
||||||
// status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
// contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
// contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// "render the index page from the router" in {
|
// "render the index page from the router" in {
|
||||||
// val request = FakeRequest(GET, "/")
|
// val request = FakeRequest(GET, "/")
|
||||||
// val home = route(app, request).get
|
// val home = route(app, request).get
|
||||||
//
|
//
|
||||||
// status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
// contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
// contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
|||||||
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1")
|
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1")
|
||||||
|
|
||||||
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2")
|
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2")
|
||||||
|
addSbtPlugin("nl.gn0s1s" % "sbt-dotenv" % "3.2.0")
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=2
|
MAJOR=4
|
||||||
MINOR=0
|
MINOR=18
|
||||||
PATCH=0
|
PATCH=1
|
||||||
|
|||||||
Reference in New Issue
Block a user