Compare commits
30 Commits
edfba93f83
...
1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e511d2a11 | ||
| 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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -138,3 +138,4 @@ target
|
|||||||
/knockoutwhistweb/.g8/
|
/knockoutwhistweb/.g8/
|
||||||
/knockoutwhistweb/.bsp/
|
/knockoutwhistweb/.bsp/
|
||||||
/currentSnapshot.json
|
/currentSnapshot.json
|
||||||
|
.env
|
||||||
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
|
||||||
126
CHANGELOG.md
126
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,63 @@
|
|||||||
|
|
||||||
* **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-14)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* **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))
|
||||||
|
|||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# === Stage 1: Build the Play application ===
|
||||||
|
FROM sbtscala/scala-sbt:eclipse-temurin-alpine-22_36_1.10.3_3.5.1 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Node.js and Less CSS preprocessor
|
||||||
|
USER root
|
||||||
|
RUN apk add --no-cache nodejs npm && \
|
||||||
|
npm install -g less
|
||||||
|
|
||||||
|
# Cache dependencies first
|
||||||
|
COPY project ./project
|
||||||
|
COPY build.sbt ./
|
||||||
|
RUN sbt -Dscoverage.skip=true update
|
||||||
|
|
||||||
|
# Copy the rest of the code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the app and stage it
|
||||||
|
RUN sbt -Dscoverage.skip=true clean stage
|
||||||
|
|
||||||
|
# === Stage 2: Runtime image ===
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
|
||||||
|
# Install Argon2 CLI and libraries
|
||||||
|
RUN apk add --no-cache bash argon2 argon2-libs
|
||||||
|
|
||||||
|
WORKDIR /opt/playapp
|
||||||
|
|
||||||
|
# Copy staged Play build
|
||||||
|
COPY --from=builder /app/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"]
|
||||||
@@ -19,7 +19,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
|
||||||
@@ -40,7 +39,8 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
|||||||
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.3.0",
|
||||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2"
|
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2",
|
||||||
|
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
lazy val root = (project in file("."))
|
||||||
|
|||||||
Submodule knockoutwhist updated: b9a7b0a2af...a5dcf3ee90
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,77 @@
|
|||||||
@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% { transform: translateX(-100vw); }
|
||||||
100% { transform: translateX(0); }
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,6 +80,15 @@
|
|||||||
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;
|
||||||
@@ -53,31 +123,21 @@
|
|||||||
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;
|
animation: slideIn 0.5s ease-out forwards;
|
||||||
height: 20%;
|
animation-fill-mode: backwards;
|
||||||
img {
|
&:nth-child(1) { animation-delay: 0.5s; }
|
||||||
animation: slideIn 0.5s ease-out forwards;
|
&:nth-child(2) { animation-delay: 1s; }
|
||||||
animation-fill-mode: backwards;
|
&:nth-child(3) { animation-delay: 1.5s; }
|
||||||
&:nth-child(1) { animation-delay: 0.5s; }
|
&:nth-child(4) { animation-delay: 2s; }
|
||||||
&:nth-child(2) { animation-delay: 1s; }
|
&:nth-child(5) { animation-delay: 2.5s; }
|
||||||
&:nth-child(3) { animation-delay: 1.5s; }
|
&:nth-child(6) { animation-delay: 3s; }
|
||||||
&:nth-child(4) { animation-delay: 2s; }
|
&:nth-child(7) { animation-delay: 3.5s; }
|
||||||
&: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;
|
||||||
@@ -111,11 +171,6 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
}
|
}
|
||||||
#trumpsuit {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-left: 4%;
|
|
||||||
}
|
|
||||||
#nextPlayers {
|
#nextPlayers {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -140,3 +195,51 @@
|
|||||||
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,7 +15,7 @@ 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] = {
|
protected def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||||
val session = request.cookies.get("sessionId")
|
val session = request.cookies.get("sessionId")
|
||||||
if (session.isDefined)
|
if (session.isDefined)
|
||||||
return sessionManager.getUserBySession(session.get.value)
|
return sessionManager.getUserBySession(session.get.value)
|
||||||
@@ -2,28 +2,26 @@ package controllers
|
|||||||
|
|
||||||
import auth.{AuthAction, AuthenticatedRequest}
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||||
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
import exceptions.*
|
||||||
import logic.PodManager
|
import logic.PodManager
|
||||||
import model.sessions.{PlayerSession, UserSession}
|
import logic.game.GameLobby
|
||||||
|
import model.sessions.UserSession
|
||||||
import play.api.*
|
import play.api.*
|
||||||
|
import play.api.libs.json.{JsValue, Json}
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
|
|
||||||
import java.util.UUID
|
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 podManager: PodManager,
|
||||||
val podManager: PodManager
|
val authAction: AuthAction,
|
||||||
) extends BaseController {
|
implicit val ec: ExecutionContext
|
||||||
|
) 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 {
|
||||||
@@ -38,12 +36,12 @@ class IngameController @Inject()(
|
|||||||
case SelectTrump =>
|
case SelectTrump =>
|
||||||
Ok(views.html.ingame.selecttrump(
|
Ok(views.html.ingame.selecttrump(
|
||||||
g.getPlayerByUser(request.user),
|
g.getPlayerByUser(request.user),
|
||||||
g.logic
|
g
|
||||||
))
|
))
|
||||||
case TieBreak =>
|
case TieBreak =>
|
||||||
Ok(views.html.ingame.tie(
|
Ok(views.html.ingame.tie(
|
||||||
g.getPlayerByUser(request.user),
|
g.getPlayerByUser(request.user),
|
||||||
g.logic
|
g
|
||||||
))
|
))
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
|
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
|
||||||
@@ -51,7 +49,6 @@ class IngameController @Inject()(
|
|||||||
case None =>
|
case None =>
|
||||||
NotFound("Game not found")
|
NotFound("Game not found")
|
||||||
}
|
}
|
||||||
//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)
|
||||||
@@ -64,30 +61,70 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
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 kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = podManager.getGame(gameId)
|
||||||
game.get.leaveGame(playerToKick)
|
val playerToKickUUID = UUID.fromString(playerToKick)
|
||||||
Redirect(routes.IngameController.game(gameId))
|
val result = Try {
|
||||||
|
game.get.leaveGame(playerToKickUUID)
|
||||||
|
}
|
||||||
|
if(result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Something went wrong."
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = podManager.getGame(gameId)
|
||||||
game.get.leaveGame(request.user.id)
|
val result = Try {
|
||||||
Redirect(routes.MainMenuController.mainMenu())
|
game.get.leaveGame(request.user.id)
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.MainMenuController.mainMenu().url
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Something went wrong."
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = podManager.getGame(gameId)
|
||||||
@@ -119,7 +156,10 @@ class IngameController @Inject()(
|
|||||||
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
|
||||||
@@ -131,27 +171,51 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
optSession.foreach(_.lock.unlock())
|
optSession.foreach(_.lock.unlock())
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
NoContent
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||||
|
))
|
||||||
} 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 _ =>
|
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"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import logic.PodManager
|
||||||
|
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,
|
||||||
|
val podManager: PodManager
|
||||||
|
) extends BaseController {
|
||||||
|
def javascriptRoutes(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
Ok(
|
||||||
|
JavaScriptReverseRouter("jsRoutes")(
|
||||||
|
routes.javascript.MainMenuController.createGame,
|
||||||
|
routes.javascript.IngameController.startGame,
|
||||||
|
routes.javascript.IngameController.kickPlayer,
|
||||||
|
routes.javascript.IngameController.leaveGame,
|
||||||
|
routes.javascript.IngameController.playCard,
|
||||||
|
routes.javascript.PollingController.polling
|
||||||
|
)
|
||||||
|
).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.*
|
||||||
@@ -29,18 +30,28 @@ class MainMenuController @Inject()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val postData = request.body.asFormUrlEncoded
|
val jsonBody = request.body.asJson
|
||||||
if (postData.isDefined) {
|
if (jsonBody.isDefined) {
|
||||||
val gamename = postData.get.get("lobbyname").flatMap(_.headOption).getOrElse(s"${request.user.name}'s Game")
|
val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String]
|
||||||
val playeramount = postData.get.get("playeramount").flatMap(_.headOption).getOrElse("")
|
.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(
|
val gameLobby = podManager.createGame(
|
||||||
host = request.user,
|
host = request.user,
|
||||||
name = gamename,
|
name = gamename,
|
||||||
maxPlayers = playeramount.toInt
|
maxPlayers = playeramount.toInt
|
||||||
)
|
)
|
||||||
Redirect(routes.IngameController.game(gameLobby.id))
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameLobby.id).url
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
BadRequest("Invalid form submission")
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Invalid form submission"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -61,9 +72,7 @@ class MainMenuController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def rules(): Action[AnyContent] = {
|
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
Action { implicit request =>
|
Ok(views.html.mainmenu.rules(Some(request.user)))
|
||||||
Ok(views.html.mainmenu.rules())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
134
knockoutwhistweb/app/controllers/PollingController.scala
Normal file
134
knockoutwhistweb/app/controllers/PollingController.scala
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import de.knockoutwhist.cards.Hand
|
||||||
|
import logic.PodManager
|
||||||
|
import logic.game.{GameLobby, PollingEvents}
|
||||||
|
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
|
||||||
|
import model.sessions.UserSession
|
||||||
|
import model.users.User
|
||||||
|
import play.api.libs.json.{JsArray, JsValue, Json}
|
||||||
|
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents, Result}
|
||||||
|
import util.WebUIUtils
|
||||||
|
|
||||||
|
import javax.inject.{Inject, Singleton}
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PollingController @Inject() (
|
||||||
|
val cc: ControllerComponents,
|
||||||
|
val podManager: PodManager,
|
||||||
|
val authAction: AuthAction,
|
||||||
|
implicit val ec: ExecutionContext
|
||||||
|
) extends AbstractController(cc) {
|
||||||
|
|
||||||
|
private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], newRound: Boolean): JsValue = {
|
||||||
|
val currentRound = game.logic.getCurrentRound.get
|
||||||
|
val currentTrick = game.logic.getCurrentTrick.get
|
||||||
|
|
||||||
|
val trickCardsJson = Json.toJson(
|
||||||
|
currentTrick.cards.map { case (card, player) =>
|
||||||
|
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val scoreTableJson = Json.toJson(
|
||||||
|
game.getLogic.getPlayerQueue.get.toList.map { player =>
|
||||||
|
Json.obj(
|
||||||
|
"name" -> player.name,
|
||||||
|
"tricks" -> currentRound.tricklist.count(_.winner.contains(player))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val stringHand = hand.map { h =>
|
||||||
|
val cardStrings = h.cards.map(WebUIUtils.cardtoString)
|
||||||
|
Json.toJson(cardStrings).as[JsArray]
|
||||||
|
}.getOrElse(Json.arr())
|
||||||
|
|
||||||
|
val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK")
|
||||||
|
val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name
|
||||||
|
Json.obj(
|
||||||
|
"status" -> "cardPlayed",
|
||||||
|
"animation" -> newRound,
|
||||||
|
"handData" -> stringHand,
|
||||||
|
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
|
||||||
|
"trumpSuit" -> currentRound.trumpSuit.toString,
|
||||||
|
"trickCards" -> trickCardsJson,
|
||||||
|
"scoreTable" -> scoreTableJson,
|
||||||
|
"firstCardId" -> firstCardId,
|
||||||
|
"nextPlayer" -> nextPlayer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def buildLobbyUsersResponse(game: GameLobby, userSession: UserSession): JsValue = {
|
||||||
|
Json.obj(
|
||||||
|
"status" -> "lobbyUpdate",
|
||||||
|
"host" -> userSession.host,
|
||||||
|
"users" -> game.getUsers.map(u => Json.obj(
|
||||||
|
"name" -> u.name,
|
||||||
|
"id" -> u.id,
|
||||||
|
"self" -> (u.id == userSession.id)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handleEvent(event: PollingEvents, game: GameLobby, userSession: UserSession): Result = {
|
||||||
|
event match {
|
||||||
|
case NewRound =>
|
||||||
|
val player = game.getPlayerByUser(userSession.user)
|
||||||
|
val hand = player.currentHand()
|
||||||
|
val jsonResponse = buildCardPlayResponse(game, hand, true)
|
||||||
|
Ok(jsonResponse)
|
||||||
|
case CardPlayed =>
|
||||||
|
val player = game.getPlayerByUser(userSession.user)
|
||||||
|
val hand = player.currentHand()
|
||||||
|
val jsonResponse = buildCardPlayResponse(game, hand, false)
|
||||||
|
Ok(jsonResponse)
|
||||||
|
case LobbyUpdate =>
|
||||||
|
Ok(buildLobbyUsersResponse(game, userSession))
|
||||||
|
case ReloadEvent =>
|
||||||
|
val jsonResponse = Json.obj(
|
||||||
|
"status" -> "reloadEvent",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(game.id).url
|
||||||
|
)
|
||||||
|
Ok(jsonResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Polling Action ---
|
||||||
|
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
|
||||||
|
val playerId = request.user.id
|
||||||
|
|
||||||
|
// 1. Safely look up the game
|
||||||
|
podManager.getGame(gameId) match {
|
||||||
|
case Some(game) =>
|
||||||
|
|
||||||
|
// 2. Short-Poll Check (Check for missed events)
|
||||||
|
if (game.getPollingState.nonEmpty) {
|
||||||
|
val event = game.getPollingState.dequeue()
|
||||||
|
|
||||||
|
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
val eventPromise = game.registerWaiter(playerId)
|
||||||
|
|
||||||
|
eventPromise.future.map { event =>
|
||||||
|
game.removeWaiter(playerId)
|
||||||
|
handleEvent(event, game, game.getUserSession(playerId))
|
||||||
|
}.recover {
|
||||||
|
case _: Throwable =>
|
||||||
|
game.removeWaiter(playerId)
|
||||||
|
NoContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case None =>
|
||||||
|
// Game not found
|
||||||
|
Future.successful(NotFound("Game not found."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,21 +2,23 @@ package logic.game
|
|||||||
|
|
||||||
import de.knockoutwhist.cards.{Hand, Suit}
|
import de.knockoutwhist.cards.{Hand, Suit}
|
||||||
import de.knockoutwhist.control.GameLogic
|
import de.knockoutwhist.control.GameLogic
|
||||||
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
|
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||||
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
|
import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed}
|
||||||
import de.knockoutwhist.events.player.PlayerEvent
|
import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent}
|
||||||
import de.knockoutwhist.player.Playertype.HUMAN
|
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 exceptions.*
|
import exceptions.*
|
||||||
|
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
|
||||||
import model.sessions.{InteractionType, UserSession}
|
import model.sessions.{InteractionType, UserSession}
|
||||||
import model.users.User
|
import model.users.User
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scala.concurrent.Promise as ScalaPromise
|
||||||
|
|
||||||
class GameLobby private(
|
class GameLobby private(
|
||||||
val logic: GameLogic,
|
val logic: GameLogic,
|
||||||
@@ -29,6 +31,18 @@ class GameLobby private(
|
|||||||
logic.createSession()
|
logic.createSession()
|
||||||
|
|
||||||
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||||
|
private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
|
||||||
|
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
|
||||||
|
|
||||||
|
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
|
||||||
|
val promise = ScalaPromise[PollingEvents]()
|
||||||
|
waitingPromises.put(playerId, promise)
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
def removeWaiter(playerId: UUID): Unit = {
|
||||||
|
waitingPromises.remove(playerId)
|
||||||
|
}
|
||||||
|
|
||||||
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!")
|
||||||
@@ -39,17 +53,28 @@ class GameLobby private(
|
|||||||
host = false
|
host = false
|
||||||
)
|
)
|
||||||
users += (user.id -> userSession)
|
users += (user.id -> userSession)
|
||||||
|
addToQueue(LobbyUpdate)
|
||||||
userSession
|
userSession
|
||||||
}
|
}
|
||||||
|
|
||||||
override def listen(event: SimpleEvent): Unit = {
|
override def listen(event: SimpleEvent): Unit = {
|
||||||
event match {
|
event match {
|
||||||
|
case event: ReceivedHandEvent =>
|
||||||
|
addToQueue(NewRound)
|
||||||
|
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||||
|
case event: CardPlayedEvent =>
|
||||||
|
addToQueue(CardPlayed)
|
||||||
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: GameStateChangeEvent =>
|
case event: GameStateChangeEvent =>
|
||||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.oldState == Lobby && event.newState == InGame) {
|
||||||
|
addToQueue(ReloadEvent)
|
||||||
|
}else {
|
||||||
|
addToQueue(ReloadEvent)
|
||||||
|
}
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
case event: SessionClosed =>
|
case event: SessionClosed =>
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
@@ -58,6 +83,15 @@ class GameLobby private(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def addToQueue(event: PollingEvents): Unit = {
|
||||||
|
if (waitingPromises.nonEmpty) {
|
||||||
|
waitingPromises.values.foreach(_.success(event))
|
||||||
|
waitingPromises.clear()
|
||||||
|
} else {
|
||||||
|
pollingState.enqueue(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -94,6 +128,7 @@ class GameLobby private(
|
|||||||
throw new NotInThisGameException("You are not in this game!")
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
}
|
}
|
||||||
users.remove(userId)
|
users.remove(userId)
|
||||||
|
addToQueue(LobbyUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,7 +218,9 @@ class GameLobby private(
|
|||||||
def getLogic: GameLogic = {
|
def getLogic: GameLogic = {
|
||||||
logic
|
logic
|
||||||
}
|
}
|
||||||
|
def getPollingState: mutable.Queue[PollingEvents] = {
|
||||||
|
pollingState
|
||||||
|
}
|
||||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||||
if (playerOption.isEmpty) {
|
if (playerOption.isEmpty) {
|
||||||
@@ -234,6 +271,10 @@ class GameLobby private(
|
|||||||
trickOpt.get
|
trickOpt.get
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getUsers: Set[User] = {
|
||||||
|
users.values.map(d => d.user).toSet
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object GameLobby {
|
object GameLobby {
|
||||||
|
|||||||
8
knockoutwhistweb/app/logic/game/PollingEvents.scala
Normal file
8
knockoutwhistweb/app/logic/game/PollingEvents.scala
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package logic.game
|
||||||
|
|
||||||
|
enum PollingEvents {
|
||||||
|
case CardPlayed
|
||||||
|
case NewRound
|
||||||
|
case ReloadEvent
|
||||||
|
case LobbyUpdate
|
||||||
|
}
|
||||||
@@ -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,15 +45,21 @@ 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 decoded = verifier.verify(sessionId)
|
val result = Try {
|
||||||
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
val decoded = verifier.verify(sessionId)
|
||||||
user.foreach(u => cache.put(sessionId, u))
|
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||||
user
|
user.foreach(u => cache.put(sessionId, u))
|
||||||
|
user
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
result.get
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 @@ import model.users.User
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
||||||
|
|
||||||
class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
class UserSession(val user: User, val host: Boolean) extends PlayerSession {
|
||||||
var canInteract: Option[InteractionType] = None
|
var canInteract: Option[InteractionType] = None
|
||||||
val lock: ReentrantLock = ReentrantLock()
|
val lock: ReentrantLock = ReentrantLock()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import scalafx.scene.image.Image
|
|||||||
|
|
||||||
object WebUIUtils {
|
object WebUIUtils {
|
||||||
def cardtoImage(card: Card): Html = {
|
def cardtoImage(card: Card): Html = {
|
||||||
|
views.html.render.card.apply(f"images/cards/${cardtoString(card)}.png")(card.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
def cardtoString(card: Card) = {
|
||||||
val s = card.suit match {
|
val s = card.suit match {
|
||||||
case Spades => "S"
|
case Spades => "S"
|
||||||
case Hearts => "H"
|
case Hearts => "H"
|
||||||
@@ -29,6 +33,7 @@ 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,93 @@
|
|||||||
|
@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
|
||||||
|
|
||||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Ingame") {
|
@main("Ingame") {
|
||||||
<div class="container">
|
<div class="lobby-background vh-100">
|
||||||
<div class="row">
|
<main class="game-field-background vh-100 ingame-side-shadow">
|
||||||
<div class="col-4 mt-5 text-start">
|
<div class="py-5 container-xxl">
|
||||||
<p class="fs-4">Next Player</p>
|
|
||||||
<p class="fs-4">@gamelobby.getLogic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
<div class="row ms-4 me-4">
|
||||||
</div>
|
<div class="col-4 mt-5 text-start">
|
||||||
<div class="col-4 mt-5 text-center">
|
<h4 class="fw-semibold mb-1">Current Player</h4>
|
||||||
<p class="fs-3">Cards played</p>
|
<p class="fs-5 text-primary" id="current-player-name">@gamelobby.getLogic.getCurrentPlayer.get.name</p>
|
||||||
</div>
|
@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {
|
||||||
<div class="col-4 mt-5 text-end">
|
<h4 class="fw-semibold mb-1">Next Player</h4>
|
||||||
<p class="fs-4">Trumpsuit:</p>
|
@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) {
|
||||||
<p class="fs-4">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
|
<p class="fs-5 text-primary" id="next-player-name">@nextplayer</p>
|
||||||
<p class="fs-4 mt-5">First Card:</p>
|
}
|
||||||
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) {
|
}
|
||||||
@util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="30%"/>
|
</div>
|
||||||
} else {
|
|
||||||
@views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="30%"/>
|
<div class="col-4 text-center">
|
||||||
}
|
|
||||||
</div>
|
<div class="score-table mt-5" id="score-table-body">
|
||||||
</div>
|
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
|
||||||
<div class="row">
|
|
||||||
<div class="col-6 mt-5">
|
<div class="d-flex justify-content-between score-header pb-1">
|
||||||
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
|
<div style="width: 50%">PLAYER</div>
|
||||||
<div class="col-auto">
|
<div style="width: 50%">TRICKS</div>
|
||||||
<div class="card" style="max-width: 8rem;">
|
|
||||||
@util.WebUIUtils.cardtoImage(cardplayed) />
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">@player</h5>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p =>
|
||||||
|
-(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size)
|
||||||
|
}) {
|
||||||
|
<div class="d-flex justify-content-between score-row pt-1">
|
||||||
|
<div style="width: 50%" class="text-truncate">@player.name</div>
|
||||||
|
<div style="width: 50%">
|
||||||
|
@(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container">
|
||||||
|
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
|
||||||
|
<div 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">
|
||||||
|
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 bg-transparent">
|
||||||
|
<small class="fw-semibold text-secondary">@player</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="col-4 mt-5 text-end">
|
||||||
</div>
|
<h4 class="fw-semibold mb-1">Trumpsuit</h4>
|
||||||
</div>
|
<p class="fs-5 text-primary" id="trumpsuit">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
|
||||||
<div class="row gx-0">
|
|
||||||
@for(i <- 0 until player.currentHand().get.cards.size) {
|
<h5 class="fw-semibold mt-4 mb-1">First Card</h5>
|
||||||
<div class="col-auto">
|
<div class="d-inline-block border rounded shadow-sm p-1 bg-light" id="first-card-container">
|
||||||
<form action="@(routes.IngameController.playCard(gamelobby.id))" class="d-flex" method="post">
|
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) {
|
||||||
<input type="hidden" name="cardId" value="@i" />
|
@util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/>
|
||||||
<button type="submit" class="btn bg-transparent p-0 m-0">
|
} else {
|
||||||
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="40%" />
|
@views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/>
|
||||||
</button>
|
}
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
</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 class="row justify-content-center ingame-cards-slide" id="card-slide">
|
||||||
|
@for(i <- player.currentHand().get.cards.indices) {
|
||||||
|
<div class="col-auto handcard" style="border-radius: 6px">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id')">
|
||||||
|
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
pollForUpdates('@gamelobby.id');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,72 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Selecting Trumpsuit...") {
|
@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">
|
||||||
</div>
|
@if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||||
<p>Your cards</p>
|
<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 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)
|
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||||
}
|
<input type="hidden" name="cardId" value="0" />
|
||||||
|
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="0" style="border-radius: 6px">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||||
|
<input type="hidden" name="cardId" value="1" />
|
||||||
|
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="1" style="border-radius: 6px">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||||
|
<input type="hidden" name="cardId" value="2" />
|
||||||
|
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="2" style="border-radius: 6px">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto handcard">
|
||||||
|
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||||
|
<input type="hidden" name="cardId" value="3" />
|
||||||
|
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="3" style="border-radius: 6px">
|
||||||
|
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
} else {
|
||||||
|
<div class="alert alert-warning" role="alert" aria-live="polite">
|
||||||
|
@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>
|
||||||
} else {
|
</div>
|
||||||
<h1>Knockout Whist</h1>
|
|
||||||
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,107 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Tie") {
|
@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">
|
||||||
</p>
|
<div class="card-header text-center">
|
||||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
<h3 class="mb-0">Tie Break</h3>
|
||||||
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p>
|
</div>
|
||||||
} else {
|
<div class="card-body">
|
||||||
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p>
|
<div class="mb-3">
|
||||||
<p>Currently picked Cards:</p>
|
<p class="card-text">
|
||||||
<div id="cardsplayed">
|
The last round was tied between:
|
||||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
<span class="ms-1">
|
||||||
<div id="playedcardplayer">
|
@for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
|
||||||
<p>@player</p>
|
<span class="badge text-bg-secondary me-1">@players</span>
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
}
|
||||||
</div>
|
</span>
|
||||||
}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
|
@if(player.equals(gamelobby.logic.playerTieLogic.currentTiePlayer())) {
|
||||||
|
@defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
|
||||||
|
<div class="alert alert-info" role="alert" aria-live="polite">
|
||||||
|
Pick a number between 1 and @{maxNum + 1}. The resulting card will be your card for the cut.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="row g-2 align-items-center" method="post" action="@routes.IngameController.playTie(gamelobby.id)">
|
||||||
|
<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 type="submit" class="btn btn-primary">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<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)
|
||||||
|
</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()</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>
|
||||||
}
|
}
|
||||||
@@ -1,84 +1,75 @@
|
|||||||
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
@main("Lobby") {
|
@main("Lobby") {
|
||||||
<div class="container">
|
<main class="lobby-background vh-100" id="lobbybackground">
|
||||||
<div class="row">
|
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
|
||||||
<div class="col">
|
<div class="row">
|
||||||
<div class="p-3 fs-1 d-flex align-items-center">
|
<div class="col">
|
||||||
<div class="text-center" style="flex-grow: 1;">
|
<div class="p-3 fs-1 d-flex align-items-center">
|
||||||
Lobby-Name: @gamelobby.name
|
<div class="text-center" style="flex-grow: 1;">
|
||||||
|
Lobby-Name: @gamelobby.name
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-danger ms-auto" onclick="leaveGame('@gamelobby.id')">Exit</div>
|
||||||
</div>
|
</div>
|
||||||
<form action="@(routes.IngameController.leaveGame(gamelobby.id))">
|
|
||||||
<button type="submit" class="btn btn-danger ms-auto">Exit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col">
|
||||||
<div class="col">
|
<div class="p-3 text-center fs-4">Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers</div>
|
||||||
<div class="p-3 text-center fs-4">Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row justify-content-center align-items-center flex-grow-1">
|
||||||
<div class="row justify-content-center">
|
@if((gamelobby.getUserSession(user.get.id).host)) {
|
||||||
|
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||||
@if((gamelobby.getUserSession(user.get.id).host)) {
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
@for(playersession <- gamelobby.getPlayers.values) {
|
<div class="col-auto my-auto m-3">
|
||||||
<div class="col-auto">
|
<div class="card" style="width: 18rem;">
|
||||||
<div class="card" style="width: 18rem;">
|
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
<div class="card-body">
|
||||||
<div class="card-body">
|
@if(playersession.id == user.get.id) {
|
||||||
|
<h5 class="card-title">@playersession.name (You)</h5>
|
||||||
|
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
||||||
|
} else {
|
||||||
|
<h5 class="card-title">@playersession.name</h5>
|
||||||
|
<div class="btn btn-danger" onclick="removePlayer('@gamelobby.id', '@playersession.id')">Remove</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-center mb-5">
|
||||||
|
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||||
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
|
<div class="col-auto my-auto m-3"> <div class="card" style="width: 18rem;">
|
||||||
|
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
|
<div class="card-body">
|
||||||
@if(playersession.id == user.get.id) {
|
@if(playersession.id == user.get.id) {
|
||||||
<h5 class="card-title">@playersession.name (You)</h5>
|
<h5 class="card-title">@playersession.name (You)</h5>
|
||||||
<p class="card-text">Your text could be here!</p>
|
|
||||||
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
|
||||||
} else {
|
} else {
|
||||||
<h5 class="card-title">@playersession.name</h5>
|
<h5 class="card-title">@playersession.name</h5>
|
||||||
<p class="card-text">Your text could be here!</p>
|
|
||||||
<form action="@(routes.IngameController.kickPlayer(gamelobby.id, playersession.id))" method="post">
|
|
||||||
<button type="submit" class="btn btn-danger">Remove</button>
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="col text-center mt-3">
|
|
||||||
<a href="@(routes.IngameController.startGame(gamelobby.id))" class="btn btn-success">Start Game</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
@for(playersession <- gamelobby.getPlayers.values) {
|
|
||||||
<div class="col-auto">
|
|
||||||
<div class="card" style="width: 18rem;">
|
|
||||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
|
||||||
<div class="card-body">
|
|
||||||
@if(playersession.id == user.get.id) {
|
|
||||||
<h5 class="card-title">@playersession.name (You)</h5>
|
|
||||||
<p class="card-text">Your text could be here!</p>
|
|
||||||
} else {
|
|
||||||
<h5 class="card-title">@playersession.name</h5>
|
|
||||||
<p class="card-text">Your text could be here!</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col mt-3">
|
|
||||||
<p class="text-center fs-4">Waiting for the host to start the game...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col mt-1">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
pollForUpdates('@gamelobby.id');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
@()
|
@()
|
||||||
|
|
||||||
@main("Login") {
|
@main("Login") {
|
||||||
|
|
||||||
<div class="login-box">
|
<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 action="@routes.UserController.login_Post()" method="post">
|
<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">
|
||||||
@@ -34,7 +35,12 @@
|
|||||||
</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');
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -3,25 +3,26 @@
|
|||||||
* 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>
|
||||||
|
|
||||||
<body>
|
<body class="d-flex flex-column min-vh-100">
|
||||||
@* 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.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
||||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
@main("Create Game") {
|
@main("Create Game") {
|
||||||
@navbar(user)
|
@navbar(user)
|
||||||
<form action="@routes.MainMenuController.createGame()" method="post">
|
<main class="lobby-background flex-grow-1">
|
||||||
<div class="w-50 mx-auto">
|
<div class="w-25 mx-auto">
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<label for="lobbyname" class="form-label">Lobby-Name</label>
|
<label for="lobbyname" class="form-label">Lobby-Name</label>
|
||||||
<input type="text" class="form-control" id="lobbyname" name="lobbyname" placeholder="Lobby 1" required>
|
<input type="text" class="form-control" id="lobbyname" name="lobbyname" placeholder="Lobby 1" required>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<label for="playeramount" class="form-label">Playeramount:</label>
|
<label for="playeramount" class="form-label">Playeramount:</label>
|
||||||
<input type="range" class="form-range" min="2" max="7" value="2" id="playeramount" name="playeramount">
|
<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">
|
<div class="d-flex justify-content-between">
|
||||||
<span>2</span>
|
<span>2</span>
|
||||||
<span>3</span>
|
<span>3</span>
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<button type="submit" class="btn btn-success">Create Game</button>
|
<div class="btn btn-success" onclick="createGameJS()">Create Game</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</main>
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
@(user: Option[model.users.User])
|
@(user: Option[model.users.User])
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-drop-shadow">
|
||||||
<div class="container-fluid">
|
<div class="container d-flex justify-content-start">
|
||||||
<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" 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>
|
||||||
|
|||||||
@@ -1,63 +1,180 @@
|
|||||||
@()
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
@main("Rules") {
|
@main("Rules") {
|
||||||
<div id="rules" class="game-field game-field-background">
|
@navbar(user)
|
||||||
<table>
|
|
||||||
<caption>Rules Overview and Equipment</caption>
|
<main class="lobby-background flex-grow-1">
|
||||||
<thead>
|
<div class="container my-4" style="max-width:980px;">
|
||||||
<tr>
|
<div class="card rules-card shadow-sm rounded-3 overflow-hidden">
|
||||||
<th>Section</th>
|
<div class="card-header text-center py-3 border-0">
|
||||||
<th>Details</th>
|
<h3 class="mb-0 rules-title">Game Rules Overview</h3>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
<tbody>
|
<div class="card-body p-0">
|
||||||
<tr>
|
<style>
|
||||||
<td>Players</td>
|
|
||||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
</style>
|
||||||
</tr>
|
|
||||||
<tr>
|
<div class="accordion rules-accordion" id="rulesAccordion">
|
||||||
<td>Aim</td>
|
<div class="accordion-item">
|
||||||
<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>
|
<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">
|
||||||
<tr>
|
Players
|
||||||
<td>Equipment</td>
|
</button>
|
||||||
<td>A standard 52-card pack is used.</td>
|
</h2>
|
||||||
</tr>
|
<div id="collapsePlayers" class="accordion-collapse collapse show" aria-labelledby="headingPlayers" data-bs-parent="#rulesAccordion">
|
||||||
<tr>
|
<div class="accordion-body">
|
||||||
<td>Card Ranks</td>
|
Two to seven players. The aim is to be the last player left in the game.
|
||||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<td>Deal (First Hand)</td>
|
|
||||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
<div class="accordion-item">
|
||||||
</tr>
|
<h2 class="accordion-header" id="headingAim">
|
||||||
<tr>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAim" aria-expanded="false" aria-controls="collapseAim">
|
||||||
<td>Deal (Subsequent Hands)</td>
|
Aim
|
||||||
<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>
|
</button>
|
||||||
</tr>
|
</h2>
|
||||||
<tr>
|
<div id="collapseAim" class="accordion-collapse collapse" aria-labelledby="headingAim" data-bs-parent="#rulesAccordion">
|
||||||
<td>Play</td>
|
<div class="accordion-body">
|
||||||
<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>
|
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>
|
||||||
<tr>
|
</div>
|
||||||
<td>Winning a Trick</td>
|
</div>
|
||||||
<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>
|
|
||||||
</tr>
|
<div class="accordion-item">
|
||||||
<tr>
|
<h2 class="accordion-header" id="headingEquipment">
|
||||||
<td>Leading Trumps</td>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEquipment" aria-expanded="false" aria-controls="collapseEquipment">
|
||||||
<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>
|
Equipment
|
||||||
</tr>
|
</button>
|
||||||
<tr>
|
</h2>
|
||||||
<td>Knockout</td>
|
<div id="collapseEquipment" class="accordion-collapse collapse" aria-labelledby="headingEquipment" data-bs-parent="#rulesAccordion">
|
||||||
<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 class="accordion-body">
|
||||||
</tr>
|
A standard 52-card pack is used.
|
||||||
<tr>
|
</div>
|
||||||
<td>Winning the Game</td>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</tr>
|
|
||||||
</tbody>
|
<div class="accordion-item">
|
||||||
<td>Dog Life</td>
|
<h2 class="accordion-header" id="headingRanks">
|
||||||
<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>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRanks" aria-expanded="false" aria-controls="collapseRanks">
|
||||||
</table>
|
Card Ranks
|
||||||
</div>
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseRanks" class="accordion-collapse collapse" aria-labelledby="headingRanks" data-bs-parent="#rulesAccordion">
|
||||||
|
<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>
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
# 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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -25,6 +26,12 @@ GET /logout controllers.UserController.logout()
|
|||||||
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)
|
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
||||||
GET /game/:id/start controllers.IngameController.startGame(id: String)
|
GET /game/:id/start controllers.IngameController.startGame(id: String)
|
||||||
POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: java.util.UUID)
|
POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String)
|
||||||
|
|
||||||
|
POST /game/:id/trump controllers.IngameController.playTrump(id: String)
|
||||||
|
POST /game/:id/tie controllers.IngameController.playTie(id: String)
|
||||||
|
|
||||||
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
|
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
|
||||||
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
||||||
|
# Polling
|
||||||
|
GET /polling controllers.PollingController.polling(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 |
@@ -1,3 +1,479 @@
|
|||||||
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 pollForUpdates(gameId) {
|
||||||
|
if (!gameId) {
|
||||||
|
console.error("Game ID is missing. Stopping poll.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const element = document.getElementById('card-slide');
|
||||||
|
const element2 = document.getElementById('lobbybackground');
|
||||||
|
// Safety check for the target element
|
||||||
|
if (!element && !element2) {
|
||||||
|
console.error("Polling target element not found. Stopping poll.");
|
||||||
|
// Use a timeout to retry in case the DOM loads late, passing gameId.
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const route = jsRoutes.controllers.PollingController.polling(gameId);
|
||||||
|
|
||||||
|
// Call your specific controller endpoint
|
||||||
|
fetch(route.url)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
console.log("Polling: Timeout reached. Restarting poll.");
|
||||||
|
|
||||||
|
// CRITICAL: Pass gameId in the recursive call
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
} else if (response.ok && response.status === 200) {
|
||||||
|
response.json().then(data => {
|
||||||
|
|
||||||
|
if (data.status === "cardPlayed" && data.handData && element) {
|
||||||
|
console.log("Event received: Card played. Redrawing hand.");
|
||||||
|
|
||||||
|
const newHand = data.handData;
|
||||||
|
let newHandHTML = '';
|
||||||
|
element.innerHTML = '';
|
||||||
|
|
||||||
|
if(data.animation) {
|
||||||
|
if (!element.classList.contains('ingame-cards-slide')) {
|
||||||
|
element.classList.add('ingame-cards-slide');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.classList.remove('ingame-cards-slide');
|
||||||
|
}
|
||||||
|
|
||||||
|
newHand.forEach((cardId, index) => {
|
||||||
|
const cardHtml = `
|
||||||
|
<div class="col-auto handcard" style="border-radius: 6px">
|
||||||
|
<div class="btn btn-outline-light p-0 border-0 shadow-none"
|
||||||
|
data-card-id="${index}"
|
||||||
|
style="border-radius: 6px"
|
||||||
|
onclick="handlePlayCard(this, '${gameId}')">
|
||||||
|
|
||||||
|
<img src="/assets/images/cards/${cardId}.png" width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
newHandHTML += cardHtml;
|
||||||
|
});
|
||||||
|
|
||||||
|
element.innerHTML = newHandHTML;
|
||||||
|
|
||||||
|
const currentPlayerElement = document.getElementById('current-player-name');
|
||||||
|
if (currentPlayerElement) {
|
||||||
|
currentPlayerElement.textContent = data.currentPlayerName;
|
||||||
|
}
|
||||||
|
const nextPlayerElement = document.getElementById('next-player-name');
|
||||||
|
if (nextPlayerElement && data.nextPlayer) {
|
||||||
|
// Use the correctly named field from the server response
|
||||||
|
nextPlayerElement.textContent = data.nextPlayer;
|
||||||
|
} else {
|
||||||
|
// Case 2: Player name is empty or null (signal to clear display).
|
||||||
|
nextPlayerElement.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const trumpElement = document.getElementById('trump-suit');
|
||||||
|
if (trumpElement) {
|
||||||
|
trumpElement.textContent = data.trumpSuit;
|
||||||
|
}
|
||||||
|
const trickContainer = document.getElementById('trick-cards-container');
|
||||||
|
if (trickContainer) {
|
||||||
|
let trickHTML = '';
|
||||||
|
|
||||||
|
// Iterate over the array of played cards received from the server
|
||||||
|
data.trickCards.forEach(trickCard => {
|
||||||
|
// Reconstruct the HTML structure from your template
|
||||||
|
trickHTML += `
|
||||||
|
<div 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="/assets/images/cards/${trickCard.cardId}.png" width="100%"/>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 bg-transparent">
|
||||||
|
<small class="fw-semibold text-secondary">${trickCard.player}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
trickContainer.innerHTML = trickHTML;
|
||||||
|
}
|
||||||
|
const scoreBody = document.getElementById('score-table-body');
|
||||||
|
if (scoreBody && data.scoreTable) {
|
||||||
|
let scoreHTML = '';
|
||||||
|
scoreHTML += `<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>`
|
||||||
|
data.scoreTable.forEach(score => {
|
||||||
|
scoreHTML += `
|
||||||
|
<div class="d-flex justify-content-between score-row pt-1">
|
||||||
|
<div style="width: 50%" class="text-truncate">${score.name}</div>
|
||||||
|
<div style="width: 50%">${score.tricks}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
scoreBody.innerHTML = scoreHTML;
|
||||||
|
}
|
||||||
|
const firstCardContainer = document.getElementById('first-card-container');
|
||||||
|
const cardId = data.firstCardId; // This will be "KH", "S7", or "BLANK"
|
||||||
|
|
||||||
|
if (firstCardContainer) {
|
||||||
|
let imageSrc = '';
|
||||||
|
let altText = 'First Card';
|
||||||
|
|
||||||
|
// Check if a card was actually played or if it's the start of a trick
|
||||||
|
if (cardId === "BLANK") {
|
||||||
|
imageSrc = "/assets/images/cards/1B.png";
|
||||||
|
altText = "Blank Card";
|
||||||
|
} else {
|
||||||
|
imageSrc = `/assets/images/cards/${cardId}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the image HTML (assuming the inner element needs replacement)
|
||||||
|
const newImageHTML = `
|
||||||
|
<img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Clear the container and insert the new image
|
||||||
|
firstCardContainer.innerHTML = newImageHTML;
|
||||||
|
}
|
||||||
|
} else if (data.status === "reloadEvent") {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
} else if (data.status === "lobbyUpdate") {
|
||||||
|
const players = document.getElementById("players");
|
||||||
|
let newHtml = ''
|
||||||
|
|
||||||
|
if (data.host) {
|
||||||
|
data.users.forEach(user => {
|
||||||
|
|
||||||
|
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>
|
||||||
|
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>`
|
||||||
|
: ` <h5 class="card-title">${user.name}</h5>
|
||||||
|
<div class="btn btn-danger" onclick="removePlayer('${gameId}', '${user.id}')">Remove</div>`
|
||||||
|
|
||||||
|
newHtml += `<div 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">
|
||||||
|
${inner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data.users.forEach(user => {
|
||||||
|
|
||||||
|
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>` : ` <h5 class="card-title">${user.name}</h5>`
|
||||||
|
|
||||||
|
newHtml += `<div 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">
|
||||||
|
${inner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
players.innerHTML = newHtml;
|
||||||
|
}
|
||||||
|
pollForUpdates(gameId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handle network or server errors
|
||||||
|
console.error(`Polling error: Status ${response.status}`);
|
||||||
|
// Wait before retrying, passing gameId correctly
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Network error during polling:", error);
|
||||||
|
// Wait before retrying on network failure, passing gameId correctly
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGameJS() {
|
||||||
|
let lobbyName = document.getElementById("lobbyname").value;
|
||||||
|
if (lobbyName === "") {
|
||||||
|
lobbyName = "DefaultLobby"
|
||||||
|
}
|
||||||
|
const playerAmount = document.getElementById("playeramount").value;
|
||||||
|
const jsonObj = {
|
||||||
|
lobbyname: lobbyName,
|
||||||
|
playeramount: playerAmount
|
||||||
|
}
|
||||||
|
sendGameCreationRequest(jsonObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendGameCreationRequest(dataObject) {
|
||||||
|
const route = jsRoutes.controllers.MainMenuController.createGame();
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(dataObject)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function startGame(gameId) {
|
||||||
|
sendGameStartRequest(gameId)
|
||||||
|
}
|
||||||
|
function sendGameStartRequest(gameId) {
|
||||||
|
const route = jsRoutes.controllers.IngameController.startGame(gameId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function removePlayer(gameid, playersessionId) {
|
||||||
|
sendRemovePlayerRequest(gameid, playersessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRemovePlayerRequest(gameId, playersessionId) {
|
||||||
|
const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveGame(gameId) {
|
||||||
|
sendLeavePlayerRequest(gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLeavePlayerRequest(gameId) {
|
||||||
|
|
||||||
|
const route = jsRoutes.controllers.IngameController.leaveGame(gameId);
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePlayCard(cardobject, gameId) {
|
||||||
|
const cardId = cardobject.dataset.cardId;
|
||||||
|
const jsonObj = {
|
||||||
|
cardID: cardId
|
||||||
|
}
|
||||||
|
sendPlayCardRequest(jsonObj, gameId, cardobject)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPlayCardRequest(jsonObj, gameId, cardobject) {
|
||||||
|
const wiggleKeyframes = [
|
||||||
|
{ transform: 'translateX(0)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(5px)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(0)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Define the timing options
|
||||||
|
const wiggleTiming = {
|
||||||
|
duration: 400, // 0.4 seconds
|
||||||
|
iterations: 1,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
// Fill mode ensures the final state is applied until reset
|
||||||
|
fill: 'forwards'
|
||||||
|
};
|
||||||
|
const route = jsRoutes.controllers.IngameController.playCard(gameId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonObj)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
//window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage.includes("You can't play this card!")) {
|
||||||
|
cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming);
|
||||||
|
} else if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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=1
|
||||||
MINOR=0
|
MINOR=1
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
Reference in New Issue
Block a user