diff --git a/build.sbt b/build.sbt
index 6051547..7f1c07e 100644
--- a/build.sbt
+++ b/build.sbt
@@ -52,6 +52,14 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
libraryDependencies += "de.janis" % "knockoutwhist-data" % "1.0-SNAPSHOT",
+ libraryDependencies += "org.hibernate.orm" % "hibernate-core" % "6.4.4.Final",
+ libraryDependencies += "jakarta.persistence" % "jakarta.persistence-api" % "3.1.0",
+ libraryDependencies += "org.postgresql" % "postgresql" % "42.7.4",
+ libraryDependencies += "org.playframework" %% "play-jdbc" % "3.0.6",
+ libraryDependencies += "org.playframework" %% "play-java-jpa" % "3.0.6",
+ libraryDependencies += "com.nimbusds" % "oauth2-oidc-sdk" % "11.31.1",
+ libraryDependencies += "org.playframework" %% "play-ws" % "3.0.6",
+ libraryDependencies += ws,
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
)
diff --git a/knockoutwhistfrontend b/knockoutwhistfrontend
index 6b8488e..86dcc61 160000
--- a/knockoutwhistfrontend
+++ b/knockoutwhistfrontend
@@ -1 +1 @@
-Subproject commit 6b8488e7a4b47c397e8e366412d32f40781e3b8b
+Subproject commit 86dcc6173c2780898803029d6c8da27c3f7590a3
diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala
deleted file mode 100644
index 8752f22..0000000
--- a/knockoutwhistweb/app/controllers/IngameController.scala
+++ /dev/null
@@ -1,444 +0,0 @@
-package controllers
-
-import auth.{AuthAction, AuthenticatedRequest}
-import de.knockoutwhist.control.GameState
-import de.knockoutwhist.control.GameState.*
-import exceptions.*
-import logic.PodManager
-import logic.game.GameLobby
-import model.sessions.UserSession
-import model.users.User
-import play.api.*
-import play.api.libs.json.{JsValue, Json}
-import play.api.mvc.*
-import play.twirl.api.Html
-import util.GameUtil
-
-import java.util.UUID
-import javax.inject.*
-import scala.concurrent.ExecutionContext
-import scala.util.Try
-
-@Singleton
-class IngameController @Inject()(
- val cc: ControllerComponents,
- val authAction: AuthAction,
- implicit val ec: ExecutionContext
- ) extends AbstractController(cc) {
-
- def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) =>
- val results = Try {
- IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user)
- }
- if (results.isSuccess) {
- Ok(views.html.main("Knockout Whist - " + GameUtil.stateToTitle(g.logic.getCurrentState))(results.get))
- } else {
- InternalServerError(results.failed.get.getMessage)
- }
- case None =>
- Redirect(routes.MainMenuController.mainMenu())
- }
- }
-
- def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- val result = Try {
- game match {
- case Some(g) =>
- g.startGame(request.user)
- case None =>
- NotFound("Game not found")
- }
- }
- if (result.isSuccess) {
- Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.IngameController.game(gameId).url,
- "content" -> IngameController.returnInnerHTML(game.get, game.get.logic.getCurrentState, request.user).toString()
- ))
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotHostException =>
- Forbidden(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotEnoughPlayersException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- }
-
- def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- val playerToKickUUID = UUID.fromString(playerToKick)
- val result = Try {
- game.get.leaveGame(playerToKickUUID, true)
- }
- 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] =>
- val game = PodManager.getGame(gameId)
- val result = Try {
- game.get.leaveGame(request.user.id, false)
- }
- if (result.isSuccess) {
- Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.MainMenuController.mainMenu().url,
- "content" -> views.html.mainmenu.creategame(Some(request.user)).toString
- ))
- } else {
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> "Something went wrong."
- ))
- }
- }
-
- def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) =>
- val jsonBody = request.body.asJson
- val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
- (jsValue \ "cardID").asOpt[String]
- }
- cardIdOpt match {
- case Some(cardId) =>
- var optSession: Option[UserSession] = None
- val result = Try {
- val session = g.getUserSession(request.user.id)
- optSession = Some(session)
- session.lock.lock()
- g.playCard(session, cardId.toInt)
- }
- optSession.foreach(_.lock.unlock())
- if (result.isSuccess) {
- Ok(Json.obj(
- "status" -> "success"
- ))
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: CantPlayCardException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalArgumentException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalStateException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotInteractableException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- case None =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> "cardId Parameter is missing"
- ))
- }
- case None =>
- NotFound(Json.obj(
- "status" -> "failure",
- "errorMessage" -> "Game not found"
- ))
- }
- }
- }
-
- def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) => {
- val jsonBody = request.body.asJson
- val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
- (jsValue \ "cardID").asOpt[String]
- }
- var optSession: Option[UserSession] = None
- val result = Try {
- cardIdOpt match {
- case Some(cardId) if cardId == "skip" =>
- val session = g.getUserSession(request.user.id)
- optSession = Some(session)
- session.lock.lock()
- g.playDogCard(session, -1)
- case Some(cardId) =>
- val session = g.getUserSession(request.user.id)
- optSession = Some(session)
- session.lock.lock()
- g.playDogCard(session, cardId.toInt)
- case None =>
- throw new IllegalArgumentException("cardId parameter is missing")
- }
- }
- optSession.foreach(_.lock.unlock())
- if (result.isSuccess) {
- NoContent
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: CantPlayCardException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalArgumentException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalStateException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- }
- case None =>
- NotFound("Game not found")
- }
- }
- }
-
- def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) =>
- val jsonBody = request.body.asJson
- val trumpOpt: Option[String] = jsonBody.flatMap { jsValue =>
- (jsValue \ "trump").asOpt[String]
- }
- trumpOpt match {
- case Some(trump) =>
- var optSession: Option[UserSession] = None
- val result = Try {
- val session = g.getUserSession(request.user.id)
- optSession = Some(session)
- session.lock.lock()
- g.selectTrump(session, trump.toInt)
- }
- optSession.foreach(_.lock.unlock())
- if (result.isSuccess) {
- NoContent
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: IllegalArgumentException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalStateException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- case None =>
- BadRequest("trump parameter is missing")
- }
- case None =>
- NotFound("Game not found")
- }
- }
-
- def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) =>
- val jsonBody = request.body.asJson
- val tieOpt: Option[String] = jsonBody.flatMap { jsValue =>
- (jsValue \ "tie").asOpt[String]
- }
- tieOpt match {
- case Some(tie) =>
- var optSession: Option[UserSession] = None
- val result = Try {
- val session = g.getUserSession(request.user.id)
- optSession = Some(session)
- session.lock.lock()
- g.selectTie(g.getUserSession(request.user.id), tie.toInt)
- }
- optSession.foreach(_.lock.unlock())
- if (result.isSuccess) {
- NoContent
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: IllegalArgumentException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalStateException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- case None =>
- BadRequest("tie parameter is missing")
- }
- case None =>
- NotFound("Game not found")
- }
- }
-
-
- def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val game = PodManager.getGame(gameId)
- game match {
- case Some(g) =>
- val result = Try {
- val session = g.getUserSession(request.user.id)
- g.returnToLobby(session)
- }
- if (result.isSuccess) {
- Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.IngameController.game(gameId).url
- ))
- } else {
- val throwable = result.failed.get
- throwable match {
- case _: NotInThisGameException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _: IllegalStateException =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- case _ =>
- InternalServerError(Json.obj(
- "status" -> "failure",
- "errorMessage" -> throwable.getMessage
- ))
- }
- }
- case None =>
- NotFound(Json.obj(
- "status" -> "failure",
- "errorMessage" -> "Game not found"
- ))
- }
- }
-
-}
-
-object IngameController {
-
- def returnInnerHTML(gameLobby: GameLobby, gameState: GameState, user: User): Html = {
- gameState match {
- case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
- case InGame =>
- views.html.ingame.ingame(
- gameLobby.getPlayerByUser(user),
- gameLobby
- )
- case SelectTrump =>
- views.html.ingame.selecttrump(
- gameLobby.getPlayerByUser(user),
- gameLobby
- )
- case TieBreak =>
- views.html.ingame.tie(
- gameLobby.getPlayerByUser(user),
- gameLobby
- )
- case FinishedMatch =>
- views.html.ingame.finishedMatch(
- Some(user),
- gameLobby
- )
- case _ =>
- throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
- }
- }
-
-}
diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
deleted file mode 100644
index 5b03508..0000000
--- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package controllers
-
-import auth.AuthAction
-import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
-import play.api.routing.JavaScriptReverseRouter
-
-import javax.inject.Inject
-
-class JavaScriptRoutingController @Inject()(
- val controllerComponents: ControllerComponents,
- val authAction: AuthAction,
- ) extends BaseController {
- def javascriptRoutes(): Action[AnyContent] =
- Action { implicit request =>
- Ok(
- JavaScriptReverseRouter("jsRoutes")(
- routes.javascript.MainMenuController.createGame,
- routes.javascript.MainMenuController.joinGame,
- routes.javascript.MainMenuController.navSPA,
- routes.javascript.UserController.login_Post
- )
- ).as("text/javascript")
- }
-}
diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala
index 8dd6cfe..e799d9c 100644
--- a/knockoutwhistweb/app/controllers/MainMenuController.scala
+++ b/knockoutwhistweb/app/controllers/MainMenuController.scala
@@ -19,15 +19,6 @@ class MainMenuController @Inject()(
val authAction: AuthAction
) extends BaseController {
- // Pass the request-handling function directly to authAction (no nested Action)
- def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Ok(views.html.main("Knockout Whist - Create Game")(views.html.mainmenu.creategame(Some(request.user))))
- }
-
- def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Redirect(routes.MainMenuController.mainMenu())
- }
-
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val jsonBody = request.body.asJson
if (jsonBody.isDefined) {
@@ -61,9 +52,7 @@ class MainMenuController @Inject()(
case Some(g) =>
g.addUser(request.user)
Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.IngameController.game(g.id).url,
- "content" -> IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user).toString
+ "status" -> "success"
))
case None =>
NotFound(Json.obj(
@@ -72,31 +61,4 @@ class MainMenuController @Inject()(
))
}
}
-
- def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
- }
-
- def navSPA(location: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- location match {
- case "0" => // Main Menu
- Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.MainMenuController.mainMenu().url,
- "content" -> views.html.mainmenu.creategame(Some(request.user)).toString
- ))
- case "1" => // Rules
- Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.MainMenuController.rules().url,
- "content" -> views.html.mainmenu.rules(Some(request.user)).toString
- ))
- case _ =>
- BadRequest(Json.obj(
- "status" -> "failure",
- "errorMessage" -> "Invalid form submission"
- ))
- }
- }
-
}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/controllers/OpenIDController.scala b/knockoutwhistweb/app/controllers/OpenIDController.scala
new file mode 100644
index 0000000..589d0d9
--- /dev/null
+++ b/knockoutwhistweb/app/controllers/OpenIDController.scala
@@ -0,0 +1,146 @@
+package controllers
+
+import logic.user.{SessionManager, UserManager}
+import model.users.User
+import play.api.Configuration
+import play.api.libs.json.Json
+import play.api.mvc.*
+import play.api.mvc.Cookie.SameSite.Lax
+import services.{OpenIDConnectService, OpenIDUserInfo}
+
+import javax.inject.*
+import scala.concurrent.{ExecutionContext, Future}
+
+@Singleton
+class OpenIDController @Inject()(
+ val controllerComponents: ControllerComponents,
+ val openIDService: OpenIDConnectService,
+ val sessionManager: SessionManager,
+ val userManager: UserManager,
+ val config: Configuration
+ )(implicit ec: ExecutionContext) extends BaseController {
+
+ def loginWithProvider(provider: String): Action[AnyContent] = Action.async { implicit request =>
+ val state = openIDService.generateState()
+ val nonce = openIDService.generateNonce()
+
+ // Store state and nonce in session
+ openIDService.getAuthorizationUrl(provider, state, nonce) match {
+ case Some(authUrl) =>
+ Future.successful(Redirect(authUrl)
+ .withSession(
+ "oauth_state" -> state,
+ "oauth_nonce" -> nonce,
+ "oauth_provider" -> provider
+ ))
+ case None =>
+ Future.successful(BadRequest(Json.obj("error" -> "Unsupported provider")))
+ }
+ }
+
+ def callback(provider: String): Action[AnyContent] = Action.async { implicit request =>
+ val sessionState = request.session.get("oauth_state")
+ val sessionNonce = request.session.get("oauth_nonce")
+ val sessionProvider = request.session.get("oauth_provider")
+
+ val returnedState = request.getQueryString("state")
+ val code = request.getQueryString("code")
+ val error = request.getQueryString("error")
+
+ error match {
+ case Some(err) =>
+ Future.successful(Redirect("/login").flashing("error" -> s"Authentication failed: $err"))
+ case None =>
+ (for {
+ _ <- Option(sessionState.contains(returnedState.getOrElse("")))
+ _ <- Option(sessionProvider.contains(provider))
+ authCode <- code
+ } yield {
+ openIDService.exchangeCodeForTokens(provider, authCode, sessionState.get).flatMap {
+ case Some(tokenResponse) =>
+ openIDService.getUserInfo(provider, tokenResponse.accessToken).map {
+ case Some(userInfo) =>
+ // Store user info in session for username selection
+ Redirect(config.get[String]("openid.selectUserRoute"))
+ .withSession(
+ "oauth_user_info" -> Json.toJson(userInfo).toString(),
+ "oauth_provider" -> provider,
+ "oauth_access_token" -> tokenResponse.accessToken
+ )
+ case None =>
+ Redirect("/login").flashing("error" -> "Failed to retrieve user information")
+ }
+ case None =>
+ Future.successful(Redirect("/login").flashing("error" -> "Failed to exchange authorization code"))
+ }
+ }).getOrElse {
+ Future.successful(Redirect("/login").flashing("error" -> "Invalid state parameter"))
+ }
+ }
+ }
+
+ def selectUsername(): Action[AnyContent] = Action.async { implicit request =>
+ request.session.get("oauth_user_info") match {
+ case Some(userInfoJson) =>
+ val userInfo = Json.parse(userInfoJson).as[OpenIDUserInfo]
+ Future.successful(Ok(Json.obj(
+ "id" -> userInfo.id,
+ "email" -> userInfo.email,
+ "name" -> userInfo.name,
+ "picture" -> userInfo.picture,
+ "provider" -> userInfo.provider,
+ "providerName" -> userInfo.providerName
+ )))
+ case None =>
+ Future.successful(Redirect("/login").flashing("error" -> "No authentication information found"))
+ }
+ }
+
+ def submitUsername(): Action[AnyContent] = Action.async { implicit request =>
+ val username = request.body.asJson.flatMap(json => (json \ "username").asOpt[String])
+ .orElse(request.body.asFormUrlEncoded.flatMap(_.get("username").flatMap(_.headOption)))
+ val userInfoJson = request.session.get("oauth_user_info")
+ val provider = request.session.get("oauth_provider").getOrElse("unknown")
+
+ (username, userInfoJson) match {
+ case (Some(uname), Some(userInfoJson)) =>
+ val userInfo = Json.parse(userInfoJson).as[OpenIDUserInfo]
+
+ // Check if username already exists
+ val trimmedUsername = uname.trim
+ userManager.userExists(trimmedUsername) match {
+ case Some(_) =>
+ Future.successful(Conflict(Json.obj("error" -> "Username already taken")))
+ case None =>
+ // Create new user with OpenID info (no password needed)
+ val success = userManager.addOpenIDUser(trimmedUsername, userInfo)
+ if (success) {
+ // Get the created user and create session
+ userManager.userExists(trimmedUsername) match {
+ case Some(user) =>
+ val sessionToken = sessionManager.createSession(user)
+ Future.successful(Ok(Json.obj(
+ "message" -> "User created successfully",
+ "user" -> Json.obj(
+ "id" -> user.id,
+ "username" -> user.name
+ )
+ )).withCookies(Cookie(
+ name = "accessToken",
+ value = sessionToken,
+ httpOnly = true,
+ secure = false,
+ sameSite = Some(Lax)
+ )).removingFromSession("oauth_user_info", "oauth_provider", "oauth_access_token"))
+ case None =>
+ Future.successful(InternalServerError(Json.obj("error" -> "Failed to create user session")))
+ }
+ } else {
+ Future.successful(InternalServerError(Json.obj("error" -> "Failed to create user")))
+ }
+ }
+ case _ =>
+ Future.successful(BadRequest(Json.obj("error" -> "Username is required")))
+ }
+ }
+}
diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala
index 392c7ff..466bf02 100644
--- a/knockoutwhistweb/app/controllers/UserController.scala
+++ b/knockoutwhistweb/app/controllers/UserController.scala
@@ -66,6 +66,47 @@ class UserController @Inject()(
))
}
+ def register(): Action[AnyContent] = {
+ Action { implicit request =>
+ val jsonBody = request.body.asJson
+ val username: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "username").asOpt[String]
+ }
+ val password: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "password").asOpt[String]
+ }
+
+ if (username.isDefined && password.isDefined) {
+ // Validate input
+ if (username.get.trim.isEmpty || password.get.length < 6) {
+ BadRequest(Json.obj(
+ "error" -> "Invalid input",
+ "message" -> "Username must not be empty and password must be at least 6 characters"
+ ))
+ } else {
+ // Try to register user
+ val registrationSuccess = userManager.addUser(username.get.trim, password.get)
+ if (registrationSuccess) {
+ Created(Json.obj(
+ "message" -> "User registered successfully",
+ "username" -> username.get.trim
+ ))
+ } else {
+ Conflict(Json.obj(
+ "error" -> "User already exists",
+ "message" -> "Username is already taken"
+ ))
+ }
+ }
+ } else {
+ BadRequest(Json.obj(
+ "error" -> "Invalid request",
+ "message" -> "Username and password are required"
+ ))
+ }
+ }
+ }
+
def logoutPost(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val sessionCookie = request.cookies.get("accessToken")
if (sessionCookie.isDefined) {
diff --git a/knockoutwhistweb/app/di/EntityManagerProvider.scala b/knockoutwhistweb/app/di/EntityManagerProvider.scala
new file mode 100644
index 0000000..9b86bb1
--- /dev/null
+++ b/knockoutwhistweb/app/di/EntityManagerProvider.scala
@@ -0,0 +1,22 @@
+package di
+
+import com.google.inject.Provider
+import com.google.inject.Inject
+import jakarta.inject.Singleton
+import jakarta.persistence.{EntityManager, EntityManagerFactory, Persistence}
+
+@Singleton
+class EntityManagerProvider @Inject()() extends Provider[EntityManager] {
+
+ private val emf: EntityManagerFactory = Persistence.createEntityManagerFactory("defaultPersistenceUnit")
+
+ override def get(): EntityManager = {
+ emf.createEntityManager()
+ }
+
+ def close(): Unit = {
+ if (emf.isOpen) {
+ emf.close()
+ }
+ }
+}
diff --git a/knockoutwhistweb/app/di/ProductionModule.scala b/knockoutwhistweb/app/di/ProductionModule.scala
new file mode 100644
index 0000000..5939616
--- /dev/null
+++ b/knockoutwhistweb/app/di/ProductionModule.scala
@@ -0,0 +1,25 @@
+package di
+
+import com.google.inject.AbstractModule
+import com.google.inject.name.Names
+import logic.user.impl.HibernateUserManager
+import play.api.db.DBApi
+import play.api.{Configuration, Environment}
+
+class ProductionModule(
+ environment: Environment,
+ configuration: Configuration
+) extends AbstractModule {
+
+ override def configure(): Unit = {
+ // Bind HibernateUserManager for production
+ bind(classOf[logic.user.UserManager])
+ .to(classOf[logic.user.impl.HibernateUserManager])
+ .asEagerSingleton()
+
+ // Bind EntityManager for JPA
+ bind(classOf[jakarta.persistence.EntityManager])
+ .toProvider(classOf[EntityManagerProvider])
+ .asEagerSingleton()
+ }
+}
diff --git a/knockoutwhistweb/app/logic/user/UserManager.scala b/knockoutwhistweb/app/logic/user/UserManager.scala
index 98e1912..cd76014 100644
--- a/knockoutwhistweb/app/logic/user/UserManager.scala
+++ b/knockoutwhistweb/app/logic/user/UserManager.scala
@@ -3,14 +3,19 @@ package logic.user
import com.google.inject.ImplementedBy
import logic.user.impl.StubUserManager
import model.users.User
+import services.OpenIDUserInfo
@ImplementedBy(classOf[StubUserManager])
trait UserManager {
def addUser(name: String, password: String): Boolean
+ def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean
+
def authenticate(name: String, password: String): Option[User]
+ def authenticateOpenID(provider: String, providerId: String): Option[User]
+
def userExists(name: String): Option[User]
def userExistsById(id: Long): Option[User]
diff --git a/knockoutwhistweb/app/logic/user/impl/HibernateUserManager.scala b/knockoutwhistweb/app/logic/user/impl/HibernateUserManager.scala
new file mode 100644
index 0000000..e61cfa0
--- /dev/null
+++ b/knockoutwhistweb/app/logic/user/impl/HibernateUserManager.scala
@@ -0,0 +1,161 @@
+package logic.user.impl
+
+import com.typesafe.config.Config
+import jakarta.inject.Inject
+import jakarta.persistence.EntityManager
+import logic.user.UserManager
+import model.users.{User, UserEntity}
+import services.OpenIDUserInfo
+import util.UserHash
+
+import javax.inject.Singleton
+import scala.jdk.CollectionConverters.*
+
+@Singleton
+class HibernateUserManager @Inject()(em: EntityManager, config: Config) extends UserManager {
+
+ override def addUser(name: String, password: String): Boolean = {
+ try {
+ // Check if user already exists
+ val existing = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
+ .setParameter("username", name)
+ .getResultList
+
+ if (!existing.isEmpty) {
+ return false
+ }
+
+ // Create new user
+ val userEntity = UserEntity.fromUser(User(
+ internalId = 0L, // Will be set by database
+ id = java.util.UUID.randomUUID(),
+ name = name,
+ passwordHash = UserHash.hashPW(password)
+ ))
+
+ em.persist(userEntity)
+ em.flush()
+ true
+ } catch {
+ case _: Exception => false
+ }
+ }
+
+ override def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean = {
+ try {
+ // Check if user already exists
+ val existing = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
+ .setParameter("username", name)
+ .getResultList
+
+ if (!existing.isEmpty) {
+ return false
+ }
+
+ // Check if OpenID user already exists
+ val existingOpenID = em.createQuery(
+ "SELECT u FROM UserEntity u WHERE u.openidProvider = :provider AND u.openidProviderId = :providerId",
+ classOf[UserEntity]
+ )
+ .setParameter("provider", userInfo.provider)
+ .setParameter("providerId", userInfo.id)
+ .getResultList
+
+ if (!existingOpenID.isEmpty) {
+ return false
+ }
+
+ // Create new OpenID user
+ val userEntity = UserEntity.fromOpenIDUser(name, userInfo)
+
+ em.persist(userEntity)
+ em.flush()
+ true
+ } catch {
+ case _: Exception => false
+ }
+ }
+
+ override def authenticate(name: String, password: String): Option[User] = {
+ try {
+ val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
+ .setParameter("username", name)
+ .getResultList
+
+ if (users.isEmpty) {
+ return None
+ }
+
+ val userEntity = users.get(0)
+ if (UserHash.verifyUser(password, userEntity.toUser)) {
+ Some(userEntity.toUser)
+ } else {
+ None
+ }
+ } catch {
+ case _: Exception => None
+ }
+ }
+
+ override def authenticateOpenID(provider: String, providerId: String): Option[User] = {
+ try {
+ val users = em.createQuery(
+ "SELECT u FROM UserEntity u WHERE u.openidProvider = :provider AND u.openidProviderId = :providerId",
+ classOf[UserEntity]
+ )
+ .setParameter("provider", provider)
+ .setParameter("providerId", providerId)
+ .getResultList
+
+ if (users.isEmpty) {
+ None
+ } else {
+ Some(users.get(0).toUser)
+ }
+ } catch {
+ case _: Exception => None
+ }
+ }
+
+ override def userExists(name: String): Option[User] = {
+ try {
+ val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
+ .setParameter("username", name)
+ .getResultList
+
+ if (users.isEmpty) {
+ None
+ } else {
+ Some(users.get(0).toUser)
+ }
+ } catch {
+ case _: Exception => None
+ }
+ }
+
+ override def userExistsById(id: Long): Option[User] = {
+ try {
+ Option(em.find(classOf[UserEntity], id)).map(_.toUser)
+ } catch {
+ case _: Exception => None
+ }
+ }
+
+ override def removeUser(name: String): Boolean = {
+ try {
+ val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
+ .setParameter("username", name)
+ .getResultList
+
+ if (users.isEmpty) {
+ false
+ } else {
+ em.remove(users.get(0))
+ em.flush()
+ true
+ }
+ } catch {
+ case _: Exception => false
+ }
+ }
+}
diff --git a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
index a50bd58..066c42e 100644
--- a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
+++ b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
@@ -3,14 +3,16 @@ package logic.user.impl
import com.typesafe.config.Config
import logic.user.UserManager
import model.users.User
+import services.OpenIDUserInfo
import util.UserHash
import javax.inject.{Inject, Singleton}
+import scala.collection.mutable
@Singleton
-class StubUserManager @Inject()(val config: Config) extends UserManager {
+class StubUserManager @Inject()(config: Config) extends UserManager {
- private val user: Map[String, User] = Map(
+ private val user: mutable.Map[String, User] = mutable.Map(
"Janis" -> User(
internalId = 1L,
id = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
@@ -19,8 +21,8 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
),
"Leon" -> User(
internalId = 2L,
- id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
- name = "Leon",
+ id = java.util.UUID.randomUUID(),
+ name = "Jakob",
passwordHash = UserHash.hashPW("password123")
),
"Jakob" -> User(
@@ -32,7 +34,26 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
)
override def addUser(name: String, password: String): Boolean = {
- throw new NotImplementedError("StubUserManager.addUser is not implemented")
+ val newUser = User(
+ internalId = user.size.toLong + 1,
+ id = java.util.UUID.randomUUID(),
+ name = name,
+ passwordHash = UserHash.hashPW(password)
+ )
+ user(name) = newUser
+ true
+ }
+
+ override def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean = {
+ // For stub implementation, just add a user without password
+ val newUser = User(
+ internalId = user.size.toLong + 1,
+ id = java.util.UUID.randomUUID(),
+ name = name,
+ passwordHash = "" // No password for OpenID users
+ )
+ user(name) = newUser
+ true
}
override def authenticate(name: String, password: String): Option[User] = {
@@ -42,6 +63,13 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
}
}
+ override def authenticateOpenID(provider: String, providerId: String): Option[User] = {
+ user.values.find { u =>
+ // In a real implementation, this would check stored OpenID provider info
+ u.name.startsWith(s"${provider}_") && u.name.contains(providerId)
+ }
+ }
+
override def userExists(name: String): Option[User] = {
user.get(name)
}
@@ -51,7 +79,6 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
}
override def removeUser(name: String): Boolean = {
- throw new NotImplementedError("StubUserManager.removeUser is not implemented")
+ user.remove(name).isDefined
}
-
}
diff --git a/knockoutwhistweb/app/model/sessions/UserSession.scala b/knockoutwhistweb/app/model/sessions/UserSession.scala
index 4d89cba..4d1b187 100644
--- a/knockoutwhistweb/app/model/sessions/UserSession.scala
+++ b/knockoutwhistweb/app/model/sessions/UserSession.scala
@@ -26,8 +26,11 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card)
case _ =>
}
+
+ lock.lock()
websocketActor.foreach(_.solveRequests())
websocketActor.foreach(_.transmitEventToClient(event))
+ lock.unlock()
}
override def id: UUID = user.id
diff --git a/knockoutwhistweb/app/model/users/UserEntity.scala b/knockoutwhistweb/app/model/users/UserEntity.scala
new file mode 100644
index 0000000..b570106
--- /dev/null
+++ b/knockoutwhistweb/app/model/users/UserEntity.scala
@@ -0,0 +1,80 @@
+package model.users
+
+import jakarta.persistence.*
+
+import java.time.LocalDateTime
+import java.util.UUID
+import scala.compiletime.uninitialized
+
+@Entity
+@Table(name = "users")
+class UserEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long = uninitialized
+
+ @Column(name = "uuid", nullable = false, unique = true)
+ var uuid: UUID = uninitialized
+
+ @Column(name = "username", nullable = false, unique = true)
+ var username: String = uninitialized
+
+ @Column(name = "password_hash", nullable = false)
+ var passwordHash: String = uninitialized
+
+ @Column(name = "openid_provider")
+ var openidProvider: String = uninitialized
+
+ @Column(name = "openid_provider_id")
+ var openidProviderId: String = uninitialized
+
+ @Column(name = "created_at", nullable = false)
+ var createdAt: LocalDateTime = uninitialized
+
+ @Column(name = "updated_at", nullable = false)
+ var updatedAt: LocalDateTime = uninitialized
+
+ @PrePersist
+ def onCreate(): Unit = {
+ val now = LocalDateTime.now()
+ createdAt = now
+ updatedAt = now
+ if (uuid == null) {
+ uuid = UUID.randomUUID()
+ }
+ }
+
+ @PreUpdate
+ def onUpdate(): Unit = {
+ updatedAt = LocalDateTime.now()
+ }
+
+ def toUser: User = {
+ User(
+ internalId = id,
+ id = uuid,
+ name = username,
+ passwordHash = passwordHash
+ )
+ }
+}
+
+object UserEntity {
+ def fromUser(user: User): UserEntity = {
+ val entity = new UserEntity()
+ entity.uuid = user.id
+ entity.username = user.name
+ entity.passwordHash = user.passwordHash
+ entity
+ }
+
+ def fromOpenIDUser(username: String, userInfo: services.OpenIDUserInfo): UserEntity = {
+ val entity = new UserEntity()
+ entity.username = username
+ entity.passwordHash = "" // No password for OpenID users
+ entity.openidProvider = userInfo.provider
+ entity.openidProviderId = userInfo.id
+ entity
+ }
+}
diff --git a/knockoutwhistweb/app/services/OpenIDConnectService.scala b/knockoutwhistweb/app/services/OpenIDConnectService.scala
new file mode 100644
index 0000000..4c4bdb9
--- /dev/null
+++ b/knockoutwhistweb/app/services/OpenIDConnectService.scala
@@ -0,0 +1,180 @@
+package services
+
+import com.typesafe.config.Config
+import play.api.libs.ws.WSClient
+import play.api.Configuration
+import play.api.libs.json.*
+
+import java.net.URI
+import javax.inject.*
+import scala.concurrent.{ExecutionContext, Future}
+import com.nimbusds.oauth2.sdk.*
+import com.nimbusds.oauth2.sdk.id.*
+import com.nimbusds.openid.connect.sdk.*
+
+import play.api.libs.ws.DefaultBodyWritables.*
+
+case class OpenIDUserInfo(
+ id: String,
+ email: Option[String],
+ name: Option[String],
+ picture: Option[String],
+ provider: String,
+ providerName: String
+)
+
+object OpenIDUserInfo {
+ implicit val writes: Writes[OpenIDUserInfo] = Json.writes[OpenIDUserInfo]
+ implicit val reads: Reads[OpenIDUserInfo] = Json.reads[OpenIDUserInfo]
+}
+
+case class OpenIDProvider(
+ name: String,
+ clientId: String,
+ clientSecret: String,
+ redirectUri: String,
+ authorizationEndpoint: String,
+ tokenEndpoint: String,
+ userInfoEndpoint: String,
+ scopes: Set[String] = Set("openid", "profile", "email")
+)
+
+case class TokenResponse(
+ accessToken: String,
+ tokenType: String,
+ expiresIn: Option[Int],
+ refreshToken: Option[String],
+ idToken: Option[String]
+)
+
+@Singleton
+class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit ec: ExecutionContext) {
+
+ private val providers = Map(
+ "discord" -> OpenIDProvider(
+ name = "Discord",
+ clientId = config.get[String]("openid.discord.clientId"),
+ clientSecret = config.get[String]("openid.discord.clientSecret"),
+ redirectUri = config.get[String]("openid.discord.redirectUri"),
+ authorizationEndpoint = "https://discord.com/oauth2/authorize",
+ tokenEndpoint = "https://discord.com/api/oauth2/token",
+ userInfoEndpoint = "https://discord.com/api/users/@me",
+ scopes = Set("identify", "email")
+ ),
+ "keycloak" -> OpenIDProvider(
+ name = "Identity",
+ clientId = config.get[String]("openid.keycloak.clientId"),
+ clientSecret = config.get[String]("openid.keycloak.clientSecret"),
+ redirectUri = config.get[String]("openid.keycloak.redirectUri"),
+ authorizationEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/auth",
+ tokenEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/token",
+ userInfoEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/userinfo",
+ scopes = Set("openid", "profile", "email")
+ )
+ )
+
+ def getAuthorizationUrl(providerName: String, state: String, nonce: String): Option[String] = {
+ providers.get(providerName).map { provider =>
+ val authRequest = if (provider.scopes.contains("openid")) {
+ // Use OpenID Connect AuthenticationRequest for OpenID providers
+ new AuthenticationRequest.Builder(
+ new ResponseType(ResponseType.Value.CODE),
+ new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")),
+ new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId),
+ URI.create(provider.redirectUri)
+ )
+ .state(new com.nimbusds.oauth2.sdk.id.State(state))
+ .nonce(new Nonce(nonce))
+ .endpointURI(URI.create(provider.authorizationEndpoint))
+ .build()
+ } else {
+ // Use standard OAuth2 AuthorizationRequest for non-OpenID providers (like Discord)
+ new AuthorizationRequest.Builder(
+ new ResponseType(ResponseType.Value.CODE),
+ new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId)
+ )
+ .scope(new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")))
+ .state(new com.nimbusds.oauth2.sdk.id.State(state))
+ .redirectionURI(URI.create(provider.redirectUri))
+ .endpointURI(URI.create(provider.authorizationEndpoint))
+ .build()
+ }
+
+ authRequest.toURI.toString
+ }
+ }
+
+ def exchangeCodeForTokens(providerName: String, code: String, state: String): Future[Option[TokenResponse]] = {
+ providers.get(providerName) match {
+ case Some(provider) =>
+ ws.url(provider.tokenEndpoint)
+ .withHttpHeaders(
+ "Accept" -> "application/json",
+ "Content-Type" -> "application/x-www-form-urlencoded"
+ )
+ .post(
+ Map(
+ "client_id" -> Seq(provider.clientId),
+ "client_secret" -> Seq(provider.clientSecret),
+ "code" -> Seq(code),
+ "grant_type" -> Seq("authorization_code"),
+ "redirect_uri" -> Seq(provider.redirectUri)
+ )
+ )
+ .map { response =>
+ if (response.status == 200) {
+ val json = response.json
+ Some(TokenResponse(
+ accessToken = (json \ "access_token").as[String],
+ tokenType = (json \ "token_type").as[String],
+ expiresIn = (json \ "expires_in").asOpt[Int],
+ refreshToken = (json \ "refresh_token").asOpt[String],
+ idToken = (json \ "id_token").asOpt[String]
+ ))
+ } else {
+ None
+ }
+ }
+ .recover { case _ => None }
+ case None => Future.successful(None)
+ }
+ }
+
+ def getUserInfo(providerName: String, accessToken: String): Future[Option[OpenIDUserInfo]] = {
+ providers.get(providerName) match {
+ case Some(provider) =>
+ ws.url(provider.userInfoEndpoint)
+ .withHttpHeaders("Authorization" -> s"Bearer $accessToken")
+ .get()
+ .map { response =>
+ if (response.status == 200) {
+ val json = response.json
+ Some(OpenIDUserInfo(
+ id = (json \ "id").as[String],
+ email = (json \ "email").asOpt[String],
+ name = (json \ "name").asOpt[String].orElse((json \ "login").asOpt[String]),
+ picture = (json \ "picture").asOpt[String].orElse((json \ "avatar_url").asOpt[String]),
+ provider = providerName,
+ providerName = provider.name
+ ))
+ } else {
+ None
+ }
+ }
+ .recover { case _ => None }
+ case None => Future.successful(None)
+ }
+ }
+
+ def validateState(sessionState: String, returnedState: String): Boolean = {
+ sessionState == returnedState
+ }
+
+ def generateState(): String = {
+ java.util.UUID.randomUUID().toString.replace("-", "")
+ }
+
+ def generateNonce(): String = {
+ java.util.UUID.randomUUID().toString.replace("-", "")
+ }
+}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/util/WebUIUtils.scala b/knockoutwhistweb/app/util/WebUIUtils.scala
index f33be6b..0539267 100644
--- a/knockoutwhistweb/app/util/WebUIUtils.scala
+++ b/knockoutwhistweb/app/util/WebUIUtils.scala
@@ -8,9 +8,6 @@ import play.twirl.api.Html
import scalafx.scene.image.Image
object WebUIUtils {
- def cardtoImage(card: Card): Html = {
- views.html.render.card.apply(cardToPath(card))(card.toString)
- }
def cardToPath(card: Card): String = {
f"images/cards/${cardtoString(card)}.png"
diff --git a/knockoutwhistweb/app/util/WebsocketEventMapper.scala b/knockoutwhistweb/app/util/WebsocketEventMapper.scala
index 8e8c2e8..76df3ba 100644
--- a/knockoutwhistweb/app/util/WebsocketEventMapper.scala
+++ b/knockoutwhistweb/app/util/WebsocketEventMapper.scala
@@ -36,16 +36,12 @@ object WebsocketEventMapper {
// Register all custom mappers here
registerCustomMapper(ReceivedHandEventMapper)
- //registerCustomMapper(GameStateEventMapper)
registerCustomMapper(CardPlayedEventMapper)
registerCustomMapper(NewRoundEventMapper)
registerCustomMapper(NewTrickEventMapper)
registerCustomMapper(TrickEndEventMapper)
registerCustomMapper(RequestCardEventMapper)
registerCustomMapper(LobbyUpdateEventMapper)
- registerCustomMapper(LeftEventMapper)
- registerCustomMapper(KickEventMapper)
- registerCustomMapper(SessionClosedMapper)
registerCustomMapper(TurnEventMapper)
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
@@ -54,7 +50,6 @@ object WebsocketEventMapper {
}else {
None
}
- //println(s"This is getting sent to client: EVENT: ${obj.id}, STATE: ${session.gameLobby.getLogic.getCurrentState.toString}, STATEDATA: ${stateToJson(session)}, DATA: ${data}")
Json.obj(
"id" -> ("request-" + java.util.UUID.randomUUID().toString),
"event" -> obj.id,
diff --git a/knockoutwhistweb/app/util/mapper/GameStateEventMapper.scala b/knockoutwhistweb/app/util/mapper/GameStateEventMapper.scala
deleted file mode 100644
index d085fa2..0000000
--- a/knockoutwhistweb/app/util/mapper/GameStateEventMapper.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package util.mapper
-
-import controllers.IngameController
-import de.knockoutwhist.events.global.GameStateChangeEvent
-import model.sessions.UserSession
-import play.api.libs.json.{JsObject, Json}
-import util.GameUtil
-
-object GameStateEventMapper extends SimpleEventMapper[GameStateChangeEvent] {
-
- override def id: String = "GameStateChangeEvent"
-
- override def toJson(event: GameStateChangeEvent, session: UserSession): JsObject = {
- Json.obj(
- "title" -> ("Knockout Whist - " + GameUtil.stateToTitle(event.newState)),
- "content" -> IngameController.returnInnerHTML(session.gameLobby, event.newState, session.user).toString
- )
- }
-}
diff --git a/knockoutwhistweb/app/util/mapper/KickEventMapper.scala b/knockoutwhistweb/app/util/mapper/KickEventMapper.scala
deleted file mode 100644
index 7f73047..0000000
--- a/knockoutwhistweb/app/util/mapper/KickEventMapper.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package util.mapper
-
-import controllers.routes
-import events.KickEvent
-import model.sessions.UserSession
-import play.api.libs.json.{JsObject, Json}
-
-object KickEventMapper extends SimpleEventMapper[KickEvent] {
-
- override def id: String = "KickEvent"
-
- override def toJson(event: KickEvent, session: UserSession): JsObject = {
- Json.obj(
- "url" -> routes.MainMenuController.mainMenu().url,
- "content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
- )
- }
-
-}
diff --git a/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala b/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala
deleted file mode 100644
index 3455dda..0000000
--- a/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package util.mapper
-
-import controllers.routes
-import events.{KickEvent, LeftEvent}
-import model.sessions.UserSession
-import play.api.libs.json.{JsObject, Json}
-
-object LeftEventMapper extends SimpleEventMapper[LeftEvent] {
-
- override def id: String = "LeftEvent"
-
- override def toJson(event: LeftEvent, session: UserSession): JsObject = {
- Json.obj(
- "url" -> routes.MainMenuController.mainMenu().url,
- "content" -> views.html.mainmenu.creategame(Some(session.user)).toString
- )
- }
-
-}
diff --git a/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala b/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala
deleted file mode 100644
index a247200..0000000
--- a/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package util.mapper
-
-import controllers.routes
-import de.knockoutwhist.events.global.SessionClosed
-import model.sessions.UserSession
-import play.api.libs.json.{JsObject, Json}
-
-object SessionClosedMapper extends SimpleEventMapper[SessionClosed] {
-
- override def id: String = "SessionClosed"
-
- override def toJson(event: SessionClosed, session: UserSession): JsObject = {
- Json.obj(
- "url" -> routes.MainMenuController.mainMenu().url,
- "content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
- )
- }
-
-}
diff --git a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
deleted file mode 100644
index 8cf76b9..0000000
--- a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
+++ /dev/null
@@ -1,108 +0,0 @@
-@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
-
-
-
-
-
-
-
-
Match Over!
-
Congratulations to the winner:
-
- @gamelobby.getLogic.getWinner.get.name
-
-
-
-
-
-
-
-
- Player
-
-
- Rounds won
- Tricks won
-
-
-
- @gamelobby.getFinalRanking.zipWithIndex.map { case ((playerName, (wonRounds, tricksWon)), index) =>
- @defining(index + 1) { rank =>
-
-
- #@rank
- @playerName
-
-
- @wonRounds
- @tricksWon
-
-
- }
- }
-
- @if(gamelobby.getFinalRanking.isEmpty) {
-
No final scores available.
- }
-
-
-
- @if(user.isDefined && gamelobby.getUserSession(user.get.id).host) {
-
- } else {
-
-
-
- Loading...
-
-
- Waiting for the Host to continue...
-
-
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html
deleted file mode 100644
index b2b39b8..0000000
--- a/knockoutwhistweb/app/views/ingame/ingame.scala.html
+++ /dev/null
@@ -1,42 +0,0 @@
-@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
-@import de.knockoutwhist.utils.Implicits.*
-
-@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
-
-
-
diff --git a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html
deleted file mode 100644
index fe9e43c..0000000
--- a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html
+++ /dev/null
@@ -1,68 +0,0 @@
-@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
-
-
-
-
-
-
-
-
-
- @if(gamelobby.logic.getCurrentMatch.isDefined) {
- @if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
-
- You (@player.toString) won the last round. Choose the trump suit for the next round.
-
-
-
-
-
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
- width="120px" 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"/>
-
-
-
-
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
- width="120px" 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"/>
-
-
-
-
- @for(i <- player.currentHand().get.cards.indices) {
-
- @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
-
- }
-
- } else {
-
- @gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name
- is choosing a trumpsuit. The new round will start once a suit is picked.
-
- }
- }
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/ingame/tie.scala.html b/knockoutwhistweb/app/views/ingame/tie.scala.html
deleted file mode 100644
index b224e1d..0000000
--- a/knockoutwhistweb/app/views/ingame/tie.scala.html
+++ /dev/null
@@ -1,114 +0,0 @@
-@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
-
-
-
-
-
-
-
-
-
-
- The last round was tied between:
-
- @for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
- @players
- }
-
-
-
-
- @if(gamelobby.logic.playerTieLogic.currentTiePlayer().contains(player)) {
- @defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
-
- Pick a number between 1 and @{
- maxNum + 1
- }.
- The resulting card will be your card for the cut.
-
-
-
-
- Your number
-
-
-
-
-
-
- Confirm
-
-
-
Currently Picked Cards
-
-
- @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
- @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
-
-
-
-
@player
-
- @util.WebUIUtils.cardtoImage(card)
-
-
-
-
- }
- } else {
-
-
-
- No cards have been selected yet.
-
-
- }
-
- }
- } else {
-
- @gamelobby.logic.playerTieLogic.currentTiePlayer().get
- is currently picking a number for the cut.
-
-
-
Currently Picked Cards
-
-
- @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
- @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
-
-
-
-
@player
-
- @util.WebUIUtils.cardtoImage(card)
-
-
-
-
- }
- } else {
-
-
-
- No cards have been selected yet.
-
-
- }
-
- }
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html
deleted file mode 100644
index e9f90a8..0000000
--- a/knockoutwhistweb/app/views/lobby/lobby.scala.html
+++ /dev/null
@@ -1,38 +0,0 @@
-@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
-@import play.api.libs.json._
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/login/login.scala.html b/knockoutwhistweb/app/views/login/login.scala.html
deleted file mode 100644
index 8be5352..0000000
--- a/knockoutwhistweb/app/views/login/login.scala.html
+++ /dev/null
@@ -1,43 +0,0 @@
-@()
-
-
-
-
Login
-
-
-
- Don’t have an account?
- Sign up
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html
deleted file mode 100644
index c7e35ec..0000000
--- a/knockoutwhistweb/app/views/main.scala.html
+++ /dev/null
@@ -1,36 +0,0 @@
-@*
-* This template is called from the `index` template. This template
-* 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`
-* object to insert into the body of the page.
-*@
-@(title: String)(content: Html)
-
-
-
-
-
-
- @* Here's where we render the page title `String`. *@
- @title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @* And here's where we render the `Html` object containing
- * the page content. *@
- @content
-
-
diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html
deleted file mode 100644
index 5ad2504..0000000
--- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html
+++ /dev/null
@@ -1,33 +0,0 @@
-@(user: Option[model.users.User])
-
-@navbar(user)
-
-
-
- Lobby-Name
-
-
-
-
- public/private
-
-
-
Playeramount:
-
-
- 2
- 3
- 4
- 5
- 6
- 7
-
-
-
-
-
-
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html
deleted file mode 100644
index 8a9ab75..0000000
--- a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html
+++ /dev/null
@@ -1,55 +0,0 @@
-@(user: Option[model.users.User])
-
-
-
-
-
-
-
-
- KnockOutWhist
-
-
-
- @* Right side: profile dropdown if logged in, otherwise Login / Sign Up buttons *@
- @if(user.isDefined) {
-
- }
-
-
-
-
diff --git a/knockoutwhistweb/app/views/mainmenu/rules.scala.html b/knockoutwhistweb/app/views/mainmenu/rules.scala.html
deleted file mode 100644
index e4ff1a4..0000000
--- a/knockoutwhistweb/app/views/mainmenu/rules.scala.html
+++ /dev/null
@@ -1,180 +0,0 @@
-@(user: Option[model.users.User])
-@navbar(user)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Two to seven players. The aim is to be the last player left in the game.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- A standard 52-card pack is used.
-
-
-
-
-
-
-
-
- In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.
-
-
-
-
-
-
-
-
- The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
-
-
diff --git a/knockoutwhistweb/app/views/render/card.scala.html b/knockoutwhistweb/app/views/render/card.scala.html
deleted file mode 100644
index a4fca9e..0000000
--- a/knockoutwhistweb/app/views/render/card.scala.html
+++ /dev/null
@@ -1,2 +0,0 @@
-@(src: String)(alt: String)
- @text
-
diff --git a/knockoutwhistweb/conf/application.conf b/knockoutwhistweb/conf/application.conf
index 3d41095..6c8444e 100644
--- a/knockoutwhistweb/conf/application.conf
+++ b/knockoutwhistweb/conf/application.conf
@@ -22,3 +22,28 @@ play.filters.cors {
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
}
+
+# Local Development OpenID Connect Configuration
+openid {
+ selectUserRoute="http://localhost:5173/select-username"
+
+ discord {
+ clientId = ${?DISCORD_CLIENT_ID}
+ clientId = "1462555597118509126"
+ clientSecret = ${?DISCORD_CLIENT_SECRET}
+ clientSecret = "xZZrdd7_tNpfJgnk-6phSG53DSTy-eMK"
+ redirectUri = ${?DISCORD_REDIRECT_URI}
+ redirectUri = "http://localhost:9000/auth/discord/callback"
+ }
+
+ keycloak {
+ clientId = ${?KEYCLOAK_CLIENT_ID}
+ clientId = "your-keycloak-client-id"
+ clientSecret = ${?KEYCLOAK_CLIENT_SECRET}
+ clientSecret = "your-keycloak-client-secret"
+ redirectUri = ${?KEYCLOAK_REDIRECT_URI}
+ redirectUri = "http://localhost:9000/auth/keycloak/callback"
+ authUrl = ${?KEYCLOAK_AUTH_URL}
+ authUrl = "http://localhost:8080/realms/master"
+ }
+}
diff --git a/knockoutwhistweb/conf/db.conf b/knockoutwhistweb/conf/db.conf
new file mode 100644
index 0000000..4e3b094
--- /dev/null
+++ b/knockoutwhistweb/conf/db.conf
@@ -0,0 +1,28 @@
+
+# Database configuration - PostgreSQL with environment variables
+db.default.driver=org.postgresql.Driver
+db.default.url=${?DATABASE_URL}
+db.default.username=${?DB_USER}
+db.default.password=${?DB_PASSWORD}
+db.default.password=""
+
+# JPA/Hibernate configuration
+jpa.default=defaultPersistenceUnit
+
+# Hibernate specific settings
+hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+hibernate.hbm2ddl.auto=update
+hibernate.show_sql=false
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
+
+# Connection pool settings
+db.default.hikaricp.maximumPoolSize=20
+db.default.hikaricp.minimumIdle=5
+db.default.hikaricp.connectionTimeout=30000
+db.default.hikaricp.idleTimeout=600000
+db.default.hikaricp.maxLifetime=1800000
+
+# PostgreSQL specific settings
+db.default.hikaricp.connectionTestQuery="SELECT 1"
+db.default.hikaricp.poolName="KnockOutWhistPool"
diff --git a/knockoutwhistweb/conf/persistence.xml b/knockoutwhistweb/conf/persistence.xml
new file mode 100644
index 0000000..7002250
--- /dev/null
+++ b/knockoutwhistweb/conf/persistence.xml
@@ -0,0 +1,38 @@
+
+
+
+
+ org.hibernate.jpa.HibernatePersistenceProvider
+
+ model.users.UserEntity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/knockoutwhistweb/conf/prod.conf b/knockoutwhistweb/conf/prod.conf
index bec057c..3ec33a9 100644
--- a/knockoutwhistweb/conf/prod.conf
+++ b/knockoutwhistweb/conf/prod.conf
@@ -1,4 +1,5 @@
include "application.conf"
+include "db.conf"
play.http.secret.key="zg8^v0R*:7-m.>^8T2B1q)sE3MV_9=M{K9zx8,<3}"
@@ -7,8 +8,29 @@ play.http.context="/api"
play.modules.enabled += "modules.GatewayModule"
play.filters.cors {
- allowedOrigins = ["https://knockout.janis-eccarius.de"]
+ allowedOrigins = ["http://localhost:5173"]
allowedCredentials = true
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
+}
+
+# OpenID Connect Configuration
+openid {
+
+ selectUserRoute="https://knockout.janis-eccarius.de/select-username"
+
+ discord {
+ clientId = ${?DISCORD_CLIENT_ID}
+ clientSecret = ${?DISCORD_CLIENT_SECRET}
+ redirectUri = ${?DISCORD_REDIRECT_URI}
+ redirectUri = "https://knockout.janis-eccarius.de/auth/discord/callback"
+ }
+
+ keycloak {
+ clientId = "your-keycloak-client-id"
+ clientSecret = "your-keycloak-client-secret"
+ redirectUri = "https://knockout.janis-eccarius.de/api/auth/keycloak/callback"
+ authUrl = ${?KEYCLOAK_AUTH_URL}
+ authUrl = "https://identity.janis-eccarius.de/realms/master"
+ }
}
\ No newline at end of file
diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes
index 2fb460e..05c920f 100644
--- a/knockoutwhistweb/conf/routes
+++ b/knockoutwhistweb/conf/routes
@@ -1,29 +1,18 @@
-# Routes
-# This file defines all application routes (Higher priority routes first)
-# https://www.playframework.com/documentation/latest/ScalaRouting
-# ~~~~
-
-# For the javascript routing
-GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
-# Primary routes
-GET / controllers.MainMenuController.index()
-GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
-
-# Main menu routes
-GET /mainmenu controllers.MainMenuController.mainMenu()
-GET /rules controllers.MainMenuController.rules()
-GET /navSPA/:pType controllers.MainMenuController.navSPA(pType)
-
+# Create game rounds
POST /createGame controllers.MainMenuController.createGame()
POST /joinGame/:gameId controllers.MainMenuController.joinGame(gameId: String)
# User authentication routes
POST /login controllers.UserController.login_Post()
+POST /register controllers.UserController.register()
POST /logout controllers.UserController.logoutPost()
GET /userInfo controllers.UserController.getUserInfo()
-# In-game routes
-GET /game/:id controllers.IngameController.game(id: String)
+# OpenID Connect routes
+GET /auth/:provider controllers.OpenIDController.loginWithProvider(provider: String)
+GET /auth/:provider/callback controllers.OpenIDController.callback(provider: String)
+GET /select-username controllers.OpenIDController.selectUsername()
+POST /submit-username controllers.OpenIDController.submitUsername()
# Websocket
GET /websocket controllers.WebsocketController.socket()
diff --git a/knockoutwhistweb/conf/staging.conf b/knockoutwhistweb/conf/staging.conf
index 32328d3..b16d2dc 100644
--- a/knockoutwhistweb/conf/staging.conf
+++ b/knockoutwhistweb/conf/staging.conf
@@ -1,4 +1,5 @@
include "application.conf"
+include "db.conf"
play.http.context="/api"
@@ -9,4 +10,24 @@ play.filters.cors {
allowedCredentials = true
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
-}
\ No newline at end of file
+}
+
+openid {
+
+ selectUserRoute="https://st.knockout.janis-eccarius.de/select-username"
+
+ discord {
+ clientId = ${?DISCORD_CLIENT_ID}
+ clientSecret = ${?DISCORD_CLIENT_SECRET}
+ redirectUri = ${?DISCORD_REDIRECT_URI}
+ redirectUri = "https://st.knockout.janis-eccarius.de/auth/discord/callback"
+ }
+
+ keycloak {
+ clientId = "your-keycloak-client-id"
+ clientSecret = "your-keycloak-client-secret"
+ redirectUri = "https://st.knockout.janis-eccarius.de/api/auth/keycloak/callback"
+ authUrl = ${?KEYCLOAK_AUTH_URL}
+ authUrl = "https://identity.janis-eccarius.de/realms/master"
+ }
+}
diff --git a/knockoutwhistweb/public/conf/particlesjs-config.json b/knockoutwhistweb/public/conf/particlesjs-config.json
deleted file mode 100644
index e0215db..0000000
--- a/knockoutwhistweb/public/conf/particlesjs-config.json
+++ /dev/null
@@ -1,110 +0,0 @@
-{
- "particles": {
- "number": {
- "value": 80,
- "density": {
- "enable": true,
- "value_area": 800
- }
- },
- "color": {
- "value": "#ffffff"
- },
- "shape": {
- "type": "circle",
- "stroke": {
- "width": 0,
- "color": "#000000"
- },
- "polygon": {
- "nb_sides": 5
- },
- "image": {
- "src": "img/github.svg",
- "width": 100,
- "height": 100
- }
- },
- "opacity": {
- "value": 0.5,
- "random": false,
- "anim": {
- "enable": false,
- "speed": 1,
- "opacity_min": 0.1,
- "sync": false
- }
- },
- "size": {
- "value": 3,
- "random": true,
- "anim": {
- "enable": false,
- "speed": 40,
- "size_min": 0.1,
- "sync": false
- }
- },
- "line_linked": {
- "enable": true,
- "distance": 150,
- "color": "#ffffff",
- "opacity": 0.4,
- "width": 1
- },
- "move": {
- "enable": true,
- "speed": 1,
- "direction": "none",
- "random": false,
- "straight": false,
- "out_mode": "out",
- "bounce": false,
- "attract": {
- "enable": false,
- "rotateX": 600,
- "rotateY": 1200
- }
- }
- },
- "interactivity": {
- "detect_on": "canvas",
- "events": {
- "onhover": {
- "enable": false,
- "mode": "repulse"
- },
- "onclick": {
- "enable": false,
- "mode": "push"
- },
- "resize": true
- },
- "modes": {
- "grab": {
- "distance": 400,
- "line_linked": {
- "opacity": 1
- }
- },
- "bubble": {
- "distance": 400,
- "size": 40,
- "duration": 2,
- "opacity": 8,
- "speed": 3
- },
- "repulse": {
- "distance": 200,
- "duration": 0.4
- },
- "push": {
- "particles_nb": 4
- },
- "remove": {
- "particles_nb": 2
- }
- }
- },
- "retina_detect": true
-}
\ No newline at end of file
diff --git a/knockoutwhistweb/public/images/background.png b/knockoutwhistweb/public/images/background.png
deleted file mode 100644
index 8a63857..0000000
Binary files a/knockoutwhistweb/public/images/background.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/1B.png b/knockoutwhistweb/public/images/cards/1B.png
deleted file mode 100644
index 56dcd3a..0000000
Binary files a/knockoutwhistweb/public/images/cards/1B.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/1J.png b/knockoutwhistweb/public/images/cards/1J.png
deleted file mode 100644
index 089ce54..0000000
Binary files a/knockoutwhistweb/public/images/cards/1J.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2B.png b/knockoutwhistweb/public/images/cards/2B.png
deleted file mode 100644
index d3338f6..0000000
Binary files a/knockoutwhistweb/public/images/cards/2B.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2C.png b/knockoutwhistweb/public/images/cards/2C.png
deleted file mode 100644
index 9782303..0000000
Binary files a/knockoutwhistweb/public/images/cards/2C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2D.png b/knockoutwhistweb/public/images/cards/2D.png
deleted file mode 100644
index fc84d48..0000000
Binary files a/knockoutwhistweb/public/images/cards/2D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2H.png b/knockoutwhistweb/public/images/cards/2H.png
deleted file mode 100644
index f258c36..0000000
Binary files a/knockoutwhistweb/public/images/cards/2H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2J.png b/knockoutwhistweb/public/images/cards/2J.png
deleted file mode 100644
index 276a83e..0000000
Binary files a/knockoutwhistweb/public/images/cards/2J.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/2S.png b/knockoutwhistweb/public/images/cards/2S.png
deleted file mode 100644
index c74b01f..0000000
Binary files a/knockoutwhistweb/public/images/cards/2S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/3C.png b/knockoutwhistweb/public/images/cards/3C.png
deleted file mode 100644
index a3ac6dd..0000000
Binary files a/knockoutwhistweb/public/images/cards/3C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/3D.png b/knockoutwhistweb/public/images/cards/3D.png
deleted file mode 100644
index 6f5642b..0000000
Binary files a/knockoutwhistweb/public/images/cards/3D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/3H.png b/knockoutwhistweb/public/images/cards/3H.png
deleted file mode 100644
index 4f10a41..0000000
Binary files a/knockoutwhistweb/public/images/cards/3H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/3S.png b/knockoutwhistweb/public/images/cards/3S.png
deleted file mode 100644
index 905e6a6..0000000
Binary files a/knockoutwhistweb/public/images/cards/3S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/4C.png b/knockoutwhistweb/public/images/cards/4C.png
deleted file mode 100644
index a1496c4..0000000
Binary files a/knockoutwhistweb/public/images/cards/4C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/4D.png b/knockoutwhistweb/public/images/cards/4D.png
deleted file mode 100644
index 8ffaa60..0000000
Binary files a/knockoutwhistweb/public/images/cards/4D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/4H.png b/knockoutwhistweb/public/images/cards/4H.png
deleted file mode 100644
index 606df63..0000000
Binary files a/knockoutwhistweb/public/images/cards/4H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/4S.png b/knockoutwhistweb/public/images/cards/4S.png
deleted file mode 100644
index 9823182..0000000
Binary files a/knockoutwhistweb/public/images/cards/4S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/5C.png b/knockoutwhistweb/public/images/cards/5C.png
deleted file mode 100644
index bfe56c5..0000000
Binary files a/knockoutwhistweb/public/images/cards/5C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/5D.png b/knockoutwhistweb/public/images/cards/5D.png
deleted file mode 100644
index 29efb30..0000000
Binary files a/knockoutwhistweb/public/images/cards/5D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/5H.png b/knockoutwhistweb/public/images/cards/5H.png
deleted file mode 100644
index 7c37348..0000000
Binary files a/knockoutwhistweb/public/images/cards/5H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/5S.png b/knockoutwhistweb/public/images/cards/5S.png
deleted file mode 100644
index 9db06c8..0000000
Binary files a/knockoutwhistweb/public/images/cards/5S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/6C.png b/knockoutwhistweb/public/images/cards/6C.png
deleted file mode 100644
index a0767c2..0000000
Binary files a/knockoutwhistweb/public/images/cards/6C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/6D.png b/knockoutwhistweb/public/images/cards/6D.png
deleted file mode 100644
index 9ee9f3b..0000000
Binary files a/knockoutwhistweb/public/images/cards/6D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/6H.png b/knockoutwhistweb/public/images/cards/6H.png
deleted file mode 100644
index 153c954..0000000
Binary files a/knockoutwhistweb/public/images/cards/6H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/6S.png b/knockoutwhistweb/public/images/cards/6S.png
deleted file mode 100644
index 9520a8b..0000000
Binary files a/knockoutwhistweb/public/images/cards/6S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/7C.png b/knockoutwhistweb/public/images/cards/7C.png
deleted file mode 100644
index 71df8c3..0000000
Binary files a/knockoutwhistweb/public/images/cards/7C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/7D.png b/knockoutwhistweb/public/images/cards/7D.png
deleted file mode 100644
index 910eac4..0000000
Binary files a/knockoutwhistweb/public/images/cards/7D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/7H.png b/knockoutwhistweb/public/images/cards/7H.png
deleted file mode 100644
index b3b3868..0000000
Binary files a/knockoutwhistweb/public/images/cards/7H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/7S.png b/knockoutwhistweb/public/images/cards/7S.png
deleted file mode 100644
index 74ac462..0000000
Binary files a/knockoutwhistweb/public/images/cards/7S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/8C.png b/knockoutwhistweb/public/images/cards/8C.png
deleted file mode 100644
index 81e275a..0000000
Binary files a/knockoutwhistweb/public/images/cards/8C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/8D.png b/knockoutwhistweb/public/images/cards/8D.png
deleted file mode 100644
index fa7784f..0000000
Binary files a/knockoutwhistweb/public/images/cards/8D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/8H.png b/knockoutwhistweb/public/images/cards/8H.png
deleted file mode 100644
index a89d9a3..0000000
Binary files a/knockoutwhistweb/public/images/cards/8H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/8S.png b/knockoutwhistweb/public/images/cards/8S.png
deleted file mode 100644
index e5cba5a..0000000
Binary files a/knockoutwhistweb/public/images/cards/8S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/9C.png b/knockoutwhistweb/public/images/cards/9C.png
deleted file mode 100644
index f4fe232..0000000
Binary files a/knockoutwhistweb/public/images/cards/9C.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/9D.png b/knockoutwhistweb/public/images/cards/9D.png
deleted file mode 100644
index fac0a45..0000000
Binary files a/knockoutwhistweb/public/images/cards/9D.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/9H.png b/knockoutwhistweb/public/images/cards/9H.png
deleted file mode 100644
index d94adcc..0000000
Binary files a/knockoutwhistweb/public/images/cards/9H.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/9S.png b/knockoutwhistweb/public/images/cards/9S.png
deleted file mode 100644
index d6e3ae8..0000000
Binary files a/knockoutwhistweb/public/images/cards/9S.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/AC.png b/knockoutwhistweb/public/images/cards/AC.png
deleted file mode 100644
index 28bf67d..0000000
Binary files a/knockoutwhistweb/public/images/cards/AC.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/ACB.png b/knockoutwhistweb/public/images/cards/ACB.png
deleted file mode 100644
index df81ced..0000000
Binary files a/knockoutwhistweb/public/images/cards/ACB.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/AD.png b/knockoutwhistweb/public/images/cards/AD.png
deleted file mode 100644
index 5b06a40..0000000
Binary files a/knockoutwhistweb/public/images/cards/AD.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/ADB.png b/knockoutwhistweb/public/images/cards/ADB.png
deleted file mode 100644
index 2619745..0000000
Binary files a/knockoutwhistweb/public/images/cards/ADB.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/AH.png b/knockoutwhistweb/public/images/cards/AH.png
deleted file mode 100644
index 92c5421..0000000
Binary files a/knockoutwhistweb/public/images/cards/AH.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/AHB.png b/knockoutwhistweb/public/images/cards/AHB.png
deleted file mode 100644
index c75de36..0000000
Binary files a/knockoutwhistweb/public/images/cards/AHB.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/AS.png b/knockoutwhistweb/public/images/cards/AS.png
deleted file mode 100644
index dc897ef..0000000
Binary files a/knockoutwhistweb/public/images/cards/AS.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/ASB.png b/knockoutwhistweb/public/images/cards/ASB.png
deleted file mode 100644
index ada726f..0000000
Binary files a/knockoutwhistweb/public/images/cards/ASB.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/JC.png b/knockoutwhistweb/public/images/cards/JC.png
deleted file mode 100644
index 878375f..0000000
Binary files a/knockoutwhistweb/public/images/cards/JC.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/JD.png b/knockoutwhistweb/public/images/cards/JD.png
deleted file mode 100644
index 73bd136..0000000
Binary files a/knockoutwhistweb/public/images/cards/JD.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/JH.png b/knockoutwhistweb/public/images/cards/JH.png
deleted file mode 100644
index 7082994..0000000
Binary files a/knockoutwhistweb/public/images/cards/JH.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/JS.png b/knockoutwhistweb/public/images/cards/JS.png
deleted file mode 100644
index 5f8a781..0000000
Binary files a/knockoutwhistweb/public/images/cards/JS.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/KC.png b/knockoutwhistweb/public/images/cards/KC.png
deleted file mode 100644
index 84ecb73..0000000
Binary files a/knockoutwhistweb/public/images/cards/KC.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/KD.png b/knockoutwhistweb/public/images/cards/KD.png
deleted file mode 100644
index 8d305e9..0000000
Binary files a/knockoutwhistweb/public/images/cards/KD.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/KH.png b/knockoutwhistweb/public/images/cards/KH.png
deleted file mode 100644
index 3eeefa1..0000000
Binary files a/knockoutwhistweb/public/images/cards/KH.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/KS.png b/knockoutwhistweb/public/images/cards/KS.png
deleted file mode 100644
index 57c19d8..0000000
Binary files a/knockoutwhistweb/public/images/cards/KS.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/QC.png b/knockoutwhistweb/public/images/cards/QC.png
deleted file mode 100644
index 97598cc..0000000
Binary files a/knockoutwhistweb/public/images/cards/QC.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/QD.png b/knockoutwhistweb/public/images/cards/QD.png
deleted file mode 100644
index 2ef96f7..0000000
Binary files a/knockoutwhistweb/public/images/cards/QD.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/QH.png b/knockoutwhistweb/public/images/cards/QH.png
deleted file mode 100644
index 6dd421e..0000000
Binary files a/knockoutwhistweb/public/images/cards/QH.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/QS.png b/knockoutwhistweb/public/images/cards/QS.png
deleted file mode 100644
index 787699d..0000000
Binary files a/knockoutwhistweb/public/images/cards/QS.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/TC.png b/knockoutwhistweb/public/images/cards/TC.png
deleted file mode 100644
index 59fb535..0000000
Binary files a/knockoutwhistweb/public/images/cards/TC.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/TD.png b/knockoutwhistweb/public/images/cards/TD.png
deleted file mode 100644
index 18d25e5..0000000
Binary files a/knockoutwhistweb/public/images/cards/TD.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/TH.png b/knockoutwhistweb/public/images/cards/TH.png
deleted file mode 100644
index 3fafa4a..0000000
Binary files a/knockoutwhistweb/public/images/cards/TH.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/cards/TS.png b/knockoutwhistweb/public/images/cards/TS.png
deleted file mode 100644
index 4246ba6..0000000
Binary files a/knockoutwhistweb/public/images/cards/TS.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/favicon.png b/knockoutwhistweb/public/images/favicon.png
deleted file mode 100644
index c7d92d2..0000000
Binary files a/knockoutwhistweb/public/images/favicon.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/img.png b/knockoutwhistweb/public/images/img.png
deleted file mode 100644
index f53382a..0000000
Binary files a/knockoutwhistweb/public/images/img.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/logo.png b/knockoutwhistweb/public/images/logo.png
deleted file mode 100644
index 0da0d37..0000000
Binary files a/knockoutwhistweb/public/images/logo.png and /dev/null differ
diff --git a/knockoutwhistweb/public/images/profile.png b/knockoutwhistweb/public/images/profile.png
deleted file mode 100644
index 941c0dc..0000000
Binary files a/knockoutwhistweb/public/images/profile.png and /dev/null differ
diff --git a/knockoutwhistweb/public/javascripts/events.js b/knockoutwhistweb/public/javascripts/events.js
deleted file mode 100644
index 7a3de21..0000000
--- a/knockoutwhistweb/public/javascripts/events.js
+++ /dev/null
@@ -1,653 +0,0 @@
-var canPlayCard = false;
-const PlayerHandComponent = {
- data() {
- return {
- hand: [],
- isDogPhase: false,
- isAwaitingResponse: false,
- };
- },
- computed: {
- isHandInactive() {
- //TODO: Needs implementation
- }
- },
- template: `
-
-
-
-
-
-
-
-
-
-
-
-
- Skip Turn
-
-
-
- `,
- methods: {
- updateHand(eventData) {
- this.hand = eventData.hand.map(card => ({
- idx: parseInt(card.idx, 10),
- card: card.card
- }));
- this.isDogPhase = false;
-
- console.log("Vue Data Updated. Hand size:", this.hand.length);
-
- if (this.hand.length > 0) {
- console.log("First card path check:", this.getCardImagePath(this.hand[0].card));
- }
- },
- handlePlayCard(cardidx) {
- if(this.isAwaitingResponse) return
- if(!canPlayCard) return
- canPlayCard = false;
- this.isAwaitingResponse = true
-
-
- console.debug(`Playing card ${cardidx} from hand`)
-
- const wiggleKeyframes = [
- { transform: 'translateX(0)' },
- { transform: 'translateX(-5px)' },
- { transform: 'translateX(5px)' },
- { transform: 'translateX(-5px)' },
- { transform: 'translateX(0)' }
- ];
-
- const wiggleTiming = {
- duration: 400,
- iterations: 1,
- easing: 'ease-in-out',
- fill: 'forwards'
- };
- const targetButton = this.$el.querySelector(`[data-card-id="${cardidx}"]`);
- const cardElement = targetButton ? targetButton.closest('.handcard') : null;
-
- const payload = {
- cardindex: cardidx.toString(),
- isDog: false
- }
- sendEventAndWait("PlayCard", payload).then(
- () => {
- this.hand = this.hand.filter(card => card.idx !== cardidx);
-
- this.hand.forEach((card, index) => {
- card.idx = index;
- })
- this.isAwaitingResponse = false;
- }
- ).catch(
- (err) => {
- if (cardElement) {
- cardElement.animate(wiggleKeyframes, wiggleTiming);
- } else {
- console.warn(`Could not find DOM element for card index ${cardidx} to wiggle.`);
- }
- this.isAwaitingResponse = false;
- canPlayCard = true;
-
- }
- )
- },
- handleSkipDogLife() {
- globalThis.handleSkipDogLife();
- },
- getCardImagePath(cardName) {
- return `/assets/images/cards/${cardName}.png`;
- }
- }
-};
-const ScoreBoardComponent = {
- data() {
- return {
- trumpsuit: 'N/A',
- playerScores: [],
- };
- },
- template: `
-
-
Tricks Won
-
-
-
-
-
-
-
- {{ player.name }}
-
-
-
- {{ player.tricks }}
-
-
-
-
- `,
-
- methods: {
- calculateNewScores(players, tricklist) {
- const playercounts = new Map();
- players.forEach(player => {
- playercounts.set(player, 0)
- });
-
- tricklist.forEach(playerWonTrick => {
- if (playerWonTrick !== "Trick in Progress" && playercounts.has(playerWonTrick)) {
- playercounts.set(playerWonTrick, playercounts.get(playerWonTrick) + 1);
- }
- });
-
- const newScores = players.map(name => ({
- name: name,
- tricks: playercounts.get(name) || 0,
- }));
-
- newScores.sort((a, b) => b.tricks - a.tricks);
-
- return newScores;
- },
-
- updateNewRoundData(eventData) {
- console.log("Vue Scoreboard Data Update Triggered: New Round!");
-
- this.playerScores = eventData.players.map(player => ({
- name: player,
- tricks: 0,
- }));
- },
-
- updateTrickEndData(eventData) {
- const { playerwon, playersin, tricklist } = eventData;
-
- console.log(`Vue Scoreboard Data Update Triggered: ${playerwon} won the trick!`);
-
- this.playerScores = this.calculateNewScores(playersin, tricklist);
-
-
- }
- }
-};
-const GameInfoComponent = {
- data() {
- return {
- trumpsuit: 'No Trumpsuit',
- firstCardImagePath: '/assets/images/cards/1B.png',
- };
- },
-
- template: `
-
-
Trumpsuit
-
{{ trumpsuit }}
-
-
-
First Card
-
-
-
-
-
- `,
-
- methods: {
- resetFirstCard(eventData) {
- console.log("GameInfoComponent: Resetting First Card to placeholder.");
- this.firstCardImagePath = '/assets/images/cards/1B.png';
- },
- updateFirstCard(eventData) {
- const firstCardId = eventData.firstCard;
- console.log("GameInfoComponent: Updating First Card to:", firstCardId);
-
- let imageSource;
- if (firstCardId === "BLANK" || !firstCardId) {
- imageSource = "/assets/images/cards/1B.png";
- } else {
- imageSource = `/assets/images/cards/${firstCardId}.png`;
- }
- this.firstCardImagePath = imageSource;
- },
- updateTrumpsuit(eventData) {
- this.trumpsuit = eventData.trumpsuit;
- }
- }
-};
-const TrickDisplayComponent = {
- data() {
- return {
- playedCards: [],
- };
- },
-
- template: `
-
-
-
-
-
-
-
- {{ play.player }}
-
-
-
-
- `,
-
- methods: {
- getCardImagePath(cardId) {
- return `/assets/images/cards/${cardId}.png`;
- },
-
- clearPlayedCards() {
- console.log("TrickDisplayComponent: Clearing played cards.");
- this.playedCards = [];
- },
-
- updatePlayedCards(eventData) {
- console.log("TrickDisplayComponent: Updating played cards.");
-
- this.playedCards = eventData.playedCards;
- }
- }
-};
-function formatPlayerName(player) {
- let name = player.name;
- if (player.dog) {
- name += " 🐶";
- }
- return name;
-}
-
-const TurnComponent = {
- data() {
- return {
- currentPlayerName: 'Waiting...',
- nextPlayers: [],
- };
- },
-
- template: `
-
-
Current Player
-
{{ currentPlayerName }}
-
-
-
- `,
- methods: {
- updateTurnData(eventData) {
- console.log("TurnComponent: Updating turn data.");
- const { currentPlayer, nextPlayers } = eventData;
-
- this.currentPlayerName = formatPlayerName(currentPlayer);
-
- this.nextPlayers = nextPlayers.map(player => formatPlayerName(player));
- }
- }
-};
-const LobbyComponent = {
- data() {
- return {
- lobbyName: 'Loading...',
- lobbyId: 'default',
- isHost: false,
- maxPlayers: 0,
- players: [],
- showKickedModal: false,
- kickedEventData: null,
- showSessionClosedModal: false,
- sessionClosedEventData: null,
- };
- },
-
- template: `
-
-
-
-
-
-
-
-
-
You've been kicked from the lobby.
-
You'll get redirected to the mainmenu in 5 seconds...
-
-
-
-
-
-
-
-
-
-
-
-
The session was closed.
-
You'll get redirected to the mainmenu in 5 seconds...
-
-
-
-
-
-
-
-
-
-
-
- Lobby-Name: {{ lobbyName }}
-
-
Exit
-
-
-
-
-
-
-
- Players: {{ players.length }} / {{ maxPlayers }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ player.name }} (You)
-
-
-
- Remove
-
-
- Remove
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ player.name }} (You)
-
-
-
-
-
-
-
-
Waiting for the host to start the game...
-
- Loading...
-
-
-
-
-
-
-
- `,
-
- methods: {
- updateLobbyData(eventData) {
- console.log("LobbyComponent: Received Lobby Update Event.");
-
- this.isHost = eventData.host;
- this.maxPlayers = eventData.maxPlayers;
- this.players = eventData.players;
- },
-
- setInitialData(name, id) {
- this.lobbyName = name;
- this.lobbyId = id;
- },
- startGame() {
- globalThis.startGame()
- },
- leaveGame(gameId) {
- //TODO: Needs implementation
- },
- handleKickPlayer(playerId) {
- globalThis.handleKickPlayer(playerId)
- },
- showKickModal(eventData) {
- this.showKickedModal = true;
- setTimeout(() => {
- this.kickedEventData = eventData;
- this.showKickedModal = false;
-
- if (typeof globalThis.receiveGameStateChange === 'function') {
- globalThis.receiveGameStateChange(this.kickedEventData);
- } else {
- console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
- }
- }, 5000);
- },
- showSessionClosedModal(eventData) {
- this.sessionClosedEventData = eventData;
- this.showSessionClosedModal = true;
-
- setTimeout(() => {
- this.showSessionClosedModal = false;
-
- if (typeof globalThis.receiveGameStateChange === 'function') {
- globalThis.receiveGameStateChange(this.sessionClosedEventData);
- } else {
- console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
- }
- }, 5000);
- }
- }
-};
-
-function requestCardEvent(eventData) {
- //TODO: Needs correct implementation of setting the inactive class in the PlayerHandComponent
-}
-function receiveGameStateChange(eventData) {
- const content = eventData.content;
- const title = eventData.title || 'Knockout Whist';
- const url = eventData.url || null;
-
- exchangeBody(content, title, url);
-}
-function receiveRoundEndEvent(eventData) {
- //TODO: When alert is working, set an alert that shows how won the round and with how much tricks.
-}
-let playerHandApp = null;
-let scoreBoardApp = null;
-let gameInfoApp = null;
-let trickDisplayApp = null;
-let turnApp = null;
-globalThis.initGameVueComponents = function() {
- // Initializing PlayerHandComponent
- const app = Vue.createApp(PlayerHandComponent);
-
- playerHandApp = app;
- const mountedHand = app.mount('#player-hand-container');
-
- if (mountedHand && mountedHand.updateHand) {
- globalThis.updatePlayerHand = mountedHand.updateHand;
- onEvent("ReceivedHandEvent", globalThis.updatePlayerHand);
- console.log("PLAYER HAND SYSTEM: updatePlayerHand successfully exposed.");
- } else {
- console.error("FATAL ERROR: PlayerHandComponent mount failed. Check if #player-hand-container exists.");
- }
-
- // Initializing Scoreboard
- if (scoreBoardApp) return
-
- const app2 = Vue.createApp(ScoreBoardComponent)
- scoreBoardApp = app2
- const mountedHand2 = app2.mount('#score-table')
- if (mountedHand2) {
- globalThis.updateNewRoundData = mountedHand2.updateNewRoundData;
- onEvent("NewRoundEvent", handleNewRoundEvent);
-
- globalThis.updateTrickEndData = mountedHand2.updateTrickEndData;
- onEvent("TrickEndEvent", globalThis.updateTrickEndData);
- console.log("SCOREBOARD: updateNewRoundData successfully exposed.");
- } else {
- console.error("FATAL ERROR: Scoreboard mount failed. Check if #score-table exists.");
- }
- // Initializing Gameinfo
- if (gameInfoApp) return
-
- const app3 = Vue.createApp(GameInfoComponent)
- gameInfoApp = app3
- const mountedGameInfo = app3.mount('#game-info-component')
- if(mountedGameInfo) {
- globalThis.resetFirstCard = mountedGameInfo.resetFirstCard;
- globalThis.updateFirstCard = mountedGameInfo.updateFirstCard;
- globalThis.updateTrumpsuit = mountedGameInfo.updateTrumpsuit
- onEvent("NewTrickEvent", handleNewTrickEvent);
- console.log("GameInfo: resetFirstCard successfully exposed.");
- } else {
- console.error("FATAL ERROR: GameInfo mount failed. Check if #score-table exists.");
- }
-
- // Initializing TrickCardContainer
- if (trickDisplayApp) return;
- const app4 = Vue.createApp(TrickDisplayComponent);
- trickDisplayApp = app4;
- const mountedTrickDisplay = app4.mount('#trick-cards-container');
-
- if (mountedTrickDisplay) {
- globalThis.clearPlayedCards = mountedTrickDisplay.clearPlayedCards;
- globalThis.updatePlayedCards = mountedTrickDisplay.updatePlayedCards;
- onEvent("CardPlayedEvent", handleCardPlayedEvent)
- console.log("TRICK DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
- } else {
- console.error("FATAL ERROR: TrickDisplay mount failed. Check if #trick-cards-container exists.");
- }
-
- // Initializing TurnContainer
- if (turnApp) return;
- const app5 = Vue.createApp(TurnComponent)
- turnApp = app5;
- const mountedTurnApp = app5.mount('#turn-component')
-
- if(mountedTurnApp) {
- globalThis.updateTurnData = mountedTurnApp.updateTurnData;
- onEvent("TurnEvent", globalThis.updateTurnData);
- console.log("TURN DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
- } else {
- console.error("FATAL ERROR: TURNAPP mount failed. Check if #trick-cards-container exists.");
- }
-}
-let lobbyApp = null;
-globalThis.initLobbyVueComponents = function(initialLobbyName, initialLobbyId, initialIsHost, initialMaxPlayers, initialPlayers) {
-
- if (lobbyApp) return;
-
- const appLobby = Vue.createApp(LobbyComponent);
- lobbyApp = appLobby;
- const mountedLobby = appLobby.mount('#lobby-app-mount');
-
- if (mountedLobby) {
- mountedLobby.setInitialData(initialLobbyName, initialLobbyId);
-
- //Damit beim erstmaligen Betreten der Lobby die Spieler etc. angezeigt werden.
- mountedLobby.updateLobbyData({
- host: initialIsHost,
- maxPlayers: initialMaxPlayers,
- players: initialPlayers
- });
-
- globalThis.updateLobbyData = mountedLobby.updateLobbyData;
- globalThis.showKickModal = mountedLobby.showKickModal;
- globalThis.showSessionClosedModal = mountedLobby.showSessionClosedModal;
- onEvent("LobbyUpdateEvent", globalThis.updateLobbyData);
- onEvent("KickEvent", globalThis.showKickModal);
- onEvent("SessionClosed", globalThis.showSessionClosedModal);
- console.log("LobbyComponent successfully mounted and registered events.");
- } else {
- console.error("FATAL ERROR: LobbyComponent mount failed.");
- }
-}
-function handleCardPlayedEvent(eventData) {
- console.log("CardPlayedEvent received. Updating Game Info and Trick Display.");
-
- if (typeof globalThis.updateFirstCard === 'function') {
- globalThis.updateFirstCard(eventData);
- }
-
- if (typeof globalThis.updatePlayedCards === 'function') {
- globalThis.updatePlayedCards(eventData);
- }
-}
-function handleNewTrickEvent(eventData) {
- if (typeof globalThis.resetFirstCard === 'function') {
- globalThis.resetFirstCard(eventData);
- }
-
- if (typeof globalThis.clearPlayedCards === 'function') {
- globalThis.clearPlayedCards();
- }
-}
-function handleNewRoundEvent(eventData) {
- if (typeof globalThis.updateNewRoundData === 'function') {
- globalThis.updateNewRoundData(eventData);
- }
- if (typeof globalThis.updateTrumpsuit === 'function') {
- globalThis.updateTrumpsuit(eventData);
- }
-}
-
-onEvent("GameStateChangeEvent", receiveGameStateChange)
-onEvent("LeftEvent", receiveGameStateChange)
-onEvent("RequestCardEvent", requestCardEvent)
-onEvent("RoundEndEvent", receiveRoundEndEvent)
\ No newline at end of file
diff --git a/knockoutwhistweb/public/javascripts/interact.js b/knockoutwhistweb/public/javascripts/interact.js
deleted file mode 100644
index c4bba34..0000000
--- a/knockoutwhistweb/public/javascripts/interact.js
+++ /dev/null
@@ -1,33 +0,0 @@
-function handlePlayCard(cardidx) {
- //TODO: Needs implementation
-}
-
-function handleSkipDogLife(button) {
- // TODO needs implementation
-}
-function startGame() {
- sendEvent("StartGame")
-}
-
-function handleTrumpSelection(object) {
- const $button = $(object);
- const trumpIndex = parseInt($button.data('trump'));
- const payload = {
- suitIndex: trumpIndex
- }
- sendEvent("PickTrumpsuit", payload)
-
-}
-function handleKickPlayer(playerId) {
- sendEvent("KickPlayer", {
- playerId: playerId
- })
-}
-function handleReturnToLobby() {
- sendEvent("ReturnToLobby")
-}
-
-globalThis.startGame = startGame
-globalThis.handleTrumpSelection = handleTrumpSelection
-globalThis.handleKickPlayer = handleKickPlayer
-globalThis.handleReturnToLobby = handleReturnToLobby
\ No newline at end of file
diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js
deleted file mode 100644
index 73b1e83..0000000
--- a/knockoutwhistweb/public/javascripts/main.js
+++ /dev/null
@@ -1,222 +0,0 @@
-/*!
- * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
- * Copyright 2011-2025 The Bootstrap Authors
- * Licensed under the Creative Commons Attribution 3.0 Unported License.
- */
-
-(() => {
- 'use strict'
-
- const getStoredTheme = () => localStorage.getItem('theme')
- const setStoredTheme = theme => localStorage.setItem('theme', theme)
-
- const getPreferredTheme = () => {
- const storedTheme = getStoredTheme()
- if (storedTheme) {
- return storedTheme
- }
-
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
- }
-
- const setTheme = theme => {
- if (theme === 'auto') {
- document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
- } else {
- document.documentElement.setAttribute('data-bs-theme', theme)
- }
- }
-
- setTheme(getPreferredTheme())
-
- const showActiveTheme = (theme, focus = false) => {
- const themeSwitcher = document.querySelector('#bd-theme')
-
- if (!themeSwitcher) {
- return
- }
-
- const themeSwitcherText = document.querySelector('#bd-theme-text')
- const activeThemeIcon = document.querySelector('.theme-icon-active use')
- const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
- const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
-
- document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
- element.classList.remove('active')
- element.setAttribute('aria-pressed', 'false')
- })
-
- btnToActive.classList.add('active')
- btnToActive.setAttribute('aria-pressed', 'true')
- activeThemeIcon.setAttribute('href', svgOfActiveBtn)
- const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
- themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
-
- if (focus) {
- themeSwitcher.focus()
- }
- }
-
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
- const storedTheme = getStoredTheme()
- if (storedTheme !== 'light' && storedTheme !== 'dark') {
- setTheme(getPreferredTheme())
- }
- })
-
- window.addEventListener('DOMContentLoaded', () => {
- showActiveTheme(getPreferredTheme())
-
- document.querySelectorAll('[data-bs-theme-value]')
- .forEach(toggle => {
- toggle.addEventListener('click', () => {
- const theme = toggle.getAttribute('data-bs-theme-value')
- setStoredTheme(theme)
- setTheme(theme)
- showActiveTheme(theme, true)
- })
- })
- })
-})()
-
-function createGameJS() {
- let lobbyName = $('#lobbyname').val();
- if ($.trim(lobbyName) === "") {
- lobbyName = "DefaultLobby"
- }
- const jsonObj = {
- lobbyname: lobbyName,
- playeramount: $("#playeramount").val()
- }
- sendGameCreationRequest(jsonObj);
-}
-
-function sendGameCreationRequest(dataObject) {
- const route = jsRoutes.controllers.MainMenuController.createGame();
-
- $.ajax({
- url: route.url,
- type: route.type,
- contentType: 'application/json',
- data: JSON.stringify(dataObject),
- dataType: 'json',
- success: (data => {
- if (data.status === 'success') {
- exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
- }
- }),
- error: ((jqXHR) => {
- const errorData = JSON.parse(jqXHR.responseText);
- if (errorData && errorData.errorMessage) {
- alert(`${errorData.errorMessage}`);
- } else {
- alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
- }
- })
- })
-}
-
-function exchangeBody(content, title = "Knockout Whist", url = null) {
- if (url) {
- window.history.pushState({}, title, url);
- }
- $("#main-body").html(content);
- document.title = title;
-}
-
-function login() {
- const username = $('#username').val();
- const password = $('#password').val();
-
- const jsonObj = {
- username: username,
- password: password
- };
-
- const route = jsRoutes.controllers.UserController.login_Post();
- $.ajax({
- url: route.url,
- type: route.type,
- contentType: 'application/json',
- dataType: 'json',
- data: JSON.stringify(jsonObj),
- success: (data => {
- if (data.status === 'success') {
- exchangeBody(data.content, 'Knockout Whist - Create Game', data.redirectUrl);
- return
- }
- alert('Login failed. Please check your credentials and try again.');
- }),
- error: ((jqXHR) => {
- const errorData = JSON.parse(jqXHR.responseText);
- if (errorData?.errorMessage) {
- alert(`${errorData.errorMessage}`);
- } else {
- alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
- }
- })
- });
-}
-
-function joinGame() {
- const gameId = $('#gameId').val();
-
- const jsonObj = {
- gameId: gameId
- };
-
- const route = jsRoutes.controllers.MainMenuController.joinGame();
- $.ajax({
- url: route.url,
- type: route.type,
- contentType: 'application/json',
- dataType: 'json',
- data: JSON.stringify(jsonObj),
- success: (data => {
- if (data.status === 'success') {
- exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
- return
- }
- alert('Could not join the game. Please check the Game ID and try again.');
- }),
- error: ((jqXHR) => {
- const errorData = JSON.parse(jqXHR.responseText);
- if (errorData?.errorMessage) {
- alert(`${errorData.errorMessage}`);
- } else {
- alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
- }
- })
- });
- return false
-}
-
-function navSpa(page, title) {
- const route = jsRoutes.controllers.MainMenuController.navSPA(page);
- $.ajax({
- url: route.url,
- type: route.type,
- contentType: 'application/json',
- dataType: 'json',
- data: JSON.stringify(jsonObj),
- success: (data => {
- if (data.status === 'success') {
- exchangeBody(data.content, title, data.redirectUrl);
- return
- }
- alert('Could not join the game. Please check the Game ID and try again.');
- }),
- error: ((jqXHR) => {
- const errorData = JSON.parse(jqXHR.responseText);
- if (errorData?.errorMessage) {
- alert(`${errorData.errorMessage}`);
- } else {
- alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
- }
- })
- });
- return false
-}
-
-
-globalThis.exchangeBody = exchangeBody;
\ No newline at end of file
diff --git a/knockoutwhistweb/public/javascripts/particles.js b/knockoutwhistweb/public/javascripts/particles.js
deleted file mode 100644
index 2a80d26..0000000
--- a/knockoutwhistweb/public/javascripts/particles.js
+++ /dev/null
@@ -1,1524 +0,0 @@
-/* -----------------------------------------------
-/* Author : Vincent Garreau - vincentgarreau.com
-/* MIT license: http://opensource.org/licenses/MIT
-/* Demo / Generator : vincentgarreau.com/particles.js
-/* GitHub : github.com/VincentGarreau/particles.js
-/* How to use? : Check the GitHub README
-/* v2.0.0
-/* ----------------------------------------------- */
-
-var pJS = function (tag_id, params) {
-
- var canvas_el = document.querySelector('#' + tag_id + ' > .particles-js-canvas-el');
-
- /* particles.js variables with default values */
- this.pJS = {
- canvas: {
- el: canvas_el,
- w: canvas_el.offsetWidth,
- h: canvas_el.offsetHeight
- },
- particles: {
- number: {
- value: 400,
- density: {
- enable: true,
- value_area: 800
- }
- },
- color: {
- value: '#fff'
- },
- shape: {
- type: 'circle',
- stroke: {
- width: 0,
- color: '#ff0000'
- },
- polygon: {
- nb_sides: 5
- },
- image: {
- src: '',
- width: 100,
- height: 100
- }
- },
- opacity: {
- value: 1,
- random: false,
- anim: {
- enable: false,
- speed: 2,
- opacity_min: 0,
- sync: false
- }
- },
- size: {
- value: 20,
- random: false,
- anim: {
- enable: false,
- speed: 20,
- size_min: 0,
- sync: false
- }
- },
- line_linked: {
- enable: true,
- distance: 100,
- color: '#fff',
- opacity: 1,
- width: 1
- },
- move: {
- enable: true,
- speed: 2,
- direction: 'none',
- random: false,
- straight: false,
- out_mode: 'out',
- bounce: false,
- attract: {
- enable: false,
- rotateX: 3000,
- rotateY: 3000
- }
- },
- array: []
- },
- interactivity: {
- detect_on: 'canvas',
- events: {
- onhover: {
- enable: true,
- mode: 'grab'
- },
- onclick: {
- enable: true,
- mode: 'push'
- },
- resize: true
- },
- modes: {
- grab: {
- distance: 100,
- line_linked: {
- opacity: 1
- }
- },
- bubble: {
- distance: 200,
- size: 80,
- duration: 0.4
- },
- repulse: {
- distance: 200,
- duration: 0.4
- },
- push: {
- particles_nb: 4
- },
- remove: {
- particles_nb: 2
- }
- },
- mouse: {}
- },
- retina_detect: false,
- fn: {
- interact: {},
- modes: {},
- vendors: {}
- },
- tmp: {}
- };
-
- var pJS = this.pJS;
-
- /* params settings */
- if (params) {
- Object.deepExtend(pJS, params);
- }
-
- pJS.tmp.obj = {
- size_value: pJS.particles.size.value,
- size_anim_speed: pJS.particles.size.anim.speed,
- move_speed: pJS.particles.move.speed,
- line_linked_distance: pJS.particles.line_linked.distance,
- line_linked_width: pJS.particles.line_linked.width,
- mode_grab_distance: pJS.interactivity.modes.grab.distance,
- mode_bubble_distance: pJS.interactivity.modes.bubble.distance,
- mode_bubble_size: pJS.interactivity.modes.bubble.size,
- mode_repulse_distance: pJS.interactivity.modes.repulse.distance
- };
-
-
- pJS.fn.retinaInit = function () {
-
- if (pJS.retina_detect && window.devicePixelRatio > 1) {
- pJS.canvas.pxratio = window.devicePixelRatio;
- pJS.tmp.retina = true;
- } else {
- pJS.canvas.pxratio = 1;
- pJS.tmp.retina = false;
- }
-
- pJS.canvas.w = pJS.canvas.el.offsetWidth * pJS.canvas.pxratio;
- pJS.canvas.h = pJS.canvas.el.offsetHeight * pJS.canvas.pxratio;
-
- pJS.particles.size.value = pJS.tmp.obj.size_value * pJS.canvas.pxratio;
- pJS.particles.size.anim.speed = pJS.tmp.obj.size_anim_speed * pJS.canvas.pxratio;
- pJS.particles.move.speed = pJS.tmp.obj.move_speed * pJS.canvas.pxratio;
- pJS.particles.line_linked.distance = pJS.tmp.obj.line_linked_distance * pJS.canvas.pxratio;
- pJS.interactivity.modes.grab.distance = pJS.tmp.obj.mode_grab_distance * pJS.canvas.pxratio;
- pJS.interactivity.modes.bubble.distance = pJS.tmp.obj.mode_bubble_distance * pJS.canvas.pxratio;
- pJS.particles.line_linked.width = pJS.tmp.obj.line_linked_width * pJS.canvas.pxratio;
- pJS.interactivity.modes.bubble.size = pJS.tmp.obj.mode_bubble_size * pJS.canvas.pxratio;
- pJS.interactivity.modes.repulse.distance = pJS.tmp.obj.mode_repulse_distance * pJS.canvas.pxratio;
-
- };
-
-
- /* ---------- pJS functions - canvas ------------ */
-
- pJS.fn.canvasInit = function () {
- pJS.canvas.ctx = pJS.canvas.el.getContext('2d');
- };
-
- pJS.fn.canvasSize = function () {
-
- pJS.canvas.el.width = pJS.canvas.w;
- pJS.canvas.el.height = pJS.canvas.h;
-
- if (pJS && pJS.interactivity.events.resize) {
-
- window.addEventListener('resize', function () {
-
- pJS.canvas.w = pJS.canvas.el.offsetWidth;
- pJS.canvas.h = pJS.canvas.el.offsetHeight;
-
- /* resize canvas */
- if (pJS.tmp.retina) {
- pJS.canvas.w *= pJS.canvas.pxratio;
- pJS.canvas.h *= pJS.canvas.pxratio;
- }
-
- pJS.canvas.el.width = pJS.canvas.w;
- pJS.canvas.el.height = pJS.canvas.h;
-
- /* repaint canvas on anim disabled */
- if (!pJS.particles.move.enable) {
- pJS.fn.particlesEmpty();
- pJS.fn.particlesCreate();
- pJS.fn.particlesDraw();
- pJS.fn.vendors.densityAutoParticles();
- }
-
- /* density particles enabled */
- pJS.fn.vendors.densityAutoParticles();
-
- });
-
- }
-
- };
-
-
- pJS.fn.canvasPaint = function () {
- pJS.canvas.ctx.fillRect(0, 0, pJS.canvas.w, pJS.canvas.h);
- };
-
- pJS.fn.canvasClear = function () {
- pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h);
- };
-
-
- /* --------- pJS functions - particles ----------- */
-
- pJS.fn.particle = function (color, opacity, position) {
-
- /* size */
- this.radius = (pJS.particles.size.random ? Math.random() : 1) * pJS.particles.size.value;
- if (pJS.particles.size.anim.enable) {
- this.size_status = false;
- this.vs = pJS.particles.size.anim.speed / 100;
- if (!pJS.particles.size.anim.sync) {
- this.vs = this.vs * Math.random();
- }
- }
-
- /* position */
- this.x = position ? position.x : Math.random() * pJS.canvas.w;
- this.y = position ? position.y : Math.random() * pJS.canvas.h;
-
- /* check position - into the canvas */
- if (this.x > pJS.canvas.w - this.radius * 2) this.x = this.x - this.radius;
- else if (this.x < this.radius * 2) this.x = this.x + this.radius;
- if (this.y > pJS.canvas.h - this.radius * 2) this.y = this.y - this.radius;
- else if (this.y < this.radius * 2) this.y = this.y + this.radius;
-
- /* check position - avoid overlap */
- if (pJS.particles.move.bounce) {
- pJS.fn.vendors.checkOverlap(this, position);
- }
-
- /* color */
- this.color = {};
- if (typeof (color.value) == 'object') {
-
- if (color.value instanceof Array) {
- var color_selected = color.value[Math.floor(Math.random() * pJS.particles.color.value.length)];
- this.color.rgb = hexToRgb(color_selected);
- } else {
- if (color.value.r != undefined && color.value.g != undefined && color.value.b != undefined) {
- this.color.rgb = {
- r: color.value.r,
- g: color.value.g,
- b: color.value.b
- }
- }
- if (color.value.h != undefined && color.value.s != undefined && color.value.l != undefined) {
- this.color.hsl = {
- h: color.value.h,
- s: color.value.s,
- l: color.value.l
- }
- }
- }
-
- } else if (color.value == 'random') {
- this.color.rgb = {
- r: (Math.floor(Math.random() * (255 - 0 + 1)) + 0),
- g: (Math.floor(Math.random() * (255 - 0 + 1)) + 0),
- b: (Math.floor(Math.random() * (255 - 0 + 1)) + 0)
- }
- } else if (typeof (color.value) == 'string') {
- this.color = color;
- this.color.rgb = hexToRgb(this.color.value);
- }
-
- /* opacity */
- this.opacity = (pJS.particles.opacity.random ? Math.random() : 1) * pJS.particles.opacity.value;
- if (pJS.particles.opacity.anim.enable) {
- this.opacity_status = false;
- this.vo = pJS.particles.opacity.anim.speed / 100;
- if (!pJS.particles.opacity.anim.sync) {
- this.vo = this.vo * Math.random();
- }
- }
-
- /* animation - velocity for speed */
- var velbase = {}
- switch (pJS.particles.move.direction) {
- case 'top':
- velbase = {x: 0, y: -1};
- break;
- case 'top-right':
- velbase = {x: 0.5, y: -0.5};
- break;
- case 'right':
- velbase = {x: 1, y: -0};
- break;
- case 'bottom-right':
- velbase = {x: 0.5, y: 0.5};
- break;
- case 'bottom':
- velbase = {x: 0, y: 1};
- break;
- case 'bottom-left':
- velbase = {x: -0.5, y: 1};
- break;
- case 'left':
- velbase = {x: -1, y: 0};
- break;
- case 'top-left':
- velbase = {x: -0.5, y: -0.5};
- break;
- default:
- velbase = {x: 0, y: 0};
- break;
- }
-
- if (pJS.particles.move.straight) {
- this.vx = velbase.x;
- this.vy = velbase.y;
- if (pJS.particles.move.random) {
- this.vx = this.vx * (Math.random());
- this.vy = this.vy * (Math.random());
- }
- } else {
- this.vx = velbase.x + Math.random() - 0.5;
- this.vy = velbase.y + Math.random() - 0.5;
- }
-
- // var theta = 2.0 * Math.PI * Math.random();
- // this.vx = Math.cos(theta);
- // this.vy = Math.sin(theta);
-
- this.vx_i = this.vx;
- this.vy_i = this.vy;
-
-
- /* if shape is image */
-
- var shape_type = pJS.particles.shape.type;
- if (typeof (shape_type) == 'object') {
- if (shape_type instanceof Array) {
- var shape_selected = shape_type[Math.floor(Math.random() * shape_type.length)];
- this.shape = shape_selected;
- }
- } else {
- this.shape = shape_type;
- }
-
- if (this.shape == 'image') {
- var sh = pJS.particles.shape;
- this.img = {
- src: sh.image.src,
- ratio: sh.image.width / sh.image.height
- }
- if (!this.img.ratio) this.img.ratio = 1;
- if (pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg != undefined) {
- pJS.fn.vendors.createSvgImg(this);
- if (pJS.tmp.pushing) {
- this.img.loaded = false;
- }
- }
- }
-
-
- };
-
-
- pJS.fn.particle.prototype.draw = function () {
-
- var p = this;
-
- if (p.radius_bubble != undefined) {
- var radius = p.radius_bubble;
- } else {
- var radius = p.radius;
- }
-
- if (p.opacity_bubble != undefined) {
- var opacity = p.opacity_bubble;
- } else {
- var opacity = p.opacity;
- }
-
- if (p.color.rgb) {
- var color_value = 'rgba(' + p.color.rgb.r + ',' + p.color.rgb.g + ',' + p.color.rgb.b + ',' + opacity + ')';
- } else {
- var color_value = 'hsla(' + p.color.hsl.h + ',' + p.color.hsl.s + '%,' + p.color.hsl.l + '%,' + opacity + ')';
- }
-
- pJS.canvas.ctx.fillStyle = color_value;
- pJS.canvas.ctx.beginPath();
-
- switch (p.shape) {
-
- case 'circle':
- pJS.canvas.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false);
- break;
-
- case 'edge':
- pJS.canvas.ctx.rect(p.x - radius, p.y - radius, radius * 2, radius * 2);
- break;
-
- case 'triangle':
- pJS.fn.vendors.drawShape(pJS.canvas.ctx, p.x - radius, p.y + radius / 1.66, radius * 2, 3, 2);
- break;
-
- case 'polygon':
- pJS.fn.vendors.drawShape(
- pJS.canvas.ctx,
- p.x - radius / (pJS.particles.shape.polygon.nb_sides / 3.5), // startX
- p.y - radius / (2.66 / 3.5), // startY
- radius * 2.66 / (pJS.particles.shape.polygon.nb_sides / 3), // sideLength
- pJS.particles.shape.polygon.nb_sides, // sideCountNumerator
- 1 // sideCountDenominator
- );
- break;
-
- case 'star':
- pJS.fn.vendors.drawShape(
- pJS.canvas.ctx,
- p.x - radius * 2 / (pJS.particles.shape.polygon.nb_sides / 4), // startX
- p.y - radius / (2 * 2.66 / 3.5), // startY
- radius * 2 * 2.66 / (pJS.particles.shape.polygon.nb_sides / 3), // sideLength
- pJS.particles.shape.polygon.nb_sides, // sideCountNumerator
- 2 // sideCountDenominator
- );
- break;
-
- case 'image':
-
- function draw() {
- pJS.canvas.ctx.drawImage(
- img_obj,
- p.x - radius,
- p.y - radius,
- radius * 2,
- radius * 2 / p.img.ratio
- );
- }
-
- if (pJS.tmp.img_type == 'svg') {
- var img_obj = p.img.obj;
- } else {
- var img_obj = pJS.tmp.img_obj;
- }
-
- if (img_obj) {
- draw();
- }
-
- break;
-
- }
-
- pJS.canvas.ctx.closePath();
-
- if (pJS.particles.shape.stroke.width > 0) {
- pJS.canvas.ctx.strokeStyle = pJS.particles.shape.stroke.color;
- pJS.canvas.ctx.lineWidth = pJS.particles.shape.stroke.width;
- pJS.canvas.ctx.stroke();
- }
-
- pJS.canvas.ctx.fill();
-
- };
-
-
- pJS.fn.particlesCreate = function () {
- for (var i = 0; i < pJS.particles.number.value; i++) {
- pJS.particles.array.push(new pJS.fn.particle(pJS.particles.color, pJS.particles.opacity.value));
- }
- };
-
- pJS.fn.particlesUpdate = function () {
-
- for (var i = 0; i < pJS.particles.array.length; i++) {
-
- /* the particle */
- var p = pJS.particles.array[i];
-
- // var d = ( dx = pJS.interactivity.mouse.click_pos_x - p.x ) * dx + ( dy = pJS.interactivity.mouse.click_pos_y - p.y ) * dy;
- // var f = -BANG_SIZE / d;
- // if ( d < BANG_SIZE ) {
- // var t = Math.atan2( dy, dx );
- // p.vx = f * Math.cos(t);
- // p.vy = f * Math.sin(t);
- // }
-
- /* move the particle */
- if (pJS.particles.move.enable) {
- var ms = pJS.particles.move.speed / 2;
- p.x += p.vx * ms;
- p.y += p.vy * ms;
- }
-
- /* change opacity status */
- if (pJS.particles.opacity.anim.enable) {
- if (p.opacity_status == true) {
- if (p.opacity >= pJS.particles.opacity.value) p.opacity_status = false;
- p.opacity += p.vo;
- } else {
- if (p.opacity <= pJS.particles.opacity.anim.opacity_min) p.opacity_status = true;
- p.opacity -= p.vo;
- }
- if (p.opacity < 0) p.opacity = 0;
- }
-
- /* change size */
- if (pJS.particles.size.anim.enable) {
- if (p.size_status == true) {
- if (p.radius >= pJS.particles.size.value) p.size_status = false;
- p.radius += p.vs;
- } else {
- if (p.radius <= pJS.particles.size.anim.size_min) p.size_status = true;
- p.radius -= p.vs;
- }
- if (p.radius < 0) p.radius = 0;
- }
-
- /* change particle position if it is out of canvas */
- if (pJS.particles.move.out_mode == 'bounce') {
- var new_pos = {
- x_left: p.radius,
- x_right: pJS.canvas.w,
- y_top: p.radius,
- y_bottom: pJS.canvas.h
- }
- } else {
- var new_pos = {
- x_left: -p.radius,
- x_right: pJS.canvas.w + p.radius,
- y_top: -p.radius,
- y_bottom: pJS.canvas.h + p.radius
- }
- }
-
- if (p.x - p.radius > pJS.canvas.w) {
- p.x = new_pos.x_left;
- p.y = Math.random() * pJS.canvas.h;
- } else if (p.x + p.radius < 0) {
- p.x = new_pos.x_right;
- p.y = Math.random() * pJS.canvas.h;
- }
- if (p.y - p.radius > pJS.canvas.h) {
- p.y = new_pos.y_top;
- p.x = Math.random() * pJS.canvas.w;
- } else if (p.y + p.radius < 0) {
- p.y = new_pos.y_bottom;
- p.x = Math.random() * pJS.canvas.w;
- }
-
- /* out of canvas modes */
- switch (pJS.particles.move.out_mode) {
- case 'bounce':
- if (p.x + p.radius > pJS.canvas.w) p.vx = -p.vx;
- else if (p.x - p.radius < 0) p.vx = -p.vx;
- if (p.y + p.radius > pJS.canvas.h) p.vy = -p.vy;
- else if (p.y - p.radius < 0) p.vy = -p.vy;
- break;
- }
-
- /* events */
- if (isInArray('grab', pJS.interactivity.events.onhover.mode)) {
- pJS.fn.modes.grabParticle(p);
- }
-
- if (isInArray('bubble', pJS.interactivity.events.onhover.mode) || isInArray('bubble', pJS.interactivity.events.onclick.mode)) {
- pJS.fn.modes.bubbleParticle(p);
- }
-
- if (isInArray('repulse', pJS.interactivity.events.onhover.mode) || isInArray('repulse', pJS.interactivity.events.onclick.mode)) {
- pJS.fn.modes.repulseParticle(p);
- }
-
- /* interaction auto between particles */
- if (pJS.particles.line_linked.enable || pJS.particles.move.attract.enable) {
- for (var j = i + 1; j < pJS.particles.array.length; j++) {
- var p2 = pJS.particles.array[j];
-
- /* link particles */
- if (pJS.particles.line_linked.enable) {
- pJS.fn.interact.linkParticles(p, p2);
- }
-
- /* attract particles */
- if (pJS.particles.move.attract.enable) {
- pJS.fn.interact.attractParticles(p, p2);
- }
-
- /* bounce particles */
- if (pJS.particles.move.bounce) {
- pJS.fn.interact.bounceParticles(p, p2);
- }
-
- }
- }
-
-
- }
-
- };
-
- pJS.fn.particlesDraw = function () {
-
- /* clear canvas */
- pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h);
-
- /* update each particles param */
- pJS.fn.particlesUpdate();
-
- /* draw each particle */
- for (var i = 0; i < pJS.particles.array.length; i++) {
- var p = pJS.particles.array[i];
- p.draw();
- }
-
- };
-
- pJS.fn.particlesEmpty = function () {
- pJS.particles.array = [];
- };
-
- pJS.fn.particlesRefresh = function () {
-
- /* init all */
- cancelRequestAnimFrame(pJS.fn.checkAnimFrame);
- cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
- pJS.tmp.source_svg = undefined;
- pJS.tmp.img_obj = undefined;
- pJS.tmp.count_svg = 0;
- pJS.fn.particlesEmpty();
- pJS.fn.canvasClear();
-
- /* restart */
- pJS.fn.vendors.start();
-
- };
-
-
- /* ---------- pJS functions - particles interaction ------------ */
-
- pJS.fn.interact.linkParticles = function (p1, p2) {
-
- var dx = p1.x - p2.x,
- dy = p1.y - p2.y,
- dist = Math.sqrt(dx * dx + dy * dy);
-
- /* draw a line between p1 and p2 if the distance between them is under the config distance */
- if (dist <= pJS.particles.line_linked.distance) {
-
- var opacity_line = pJS.particles.line_linked.opacity - (dist / (1 / pJS.particles.line_linked.opacity)) / pJS.particles.line_linked.distance;
-
- if (opacity_line > 0) {
-
- /* style */
- var color_line = pJS.particles.line_linked.color_rgb_line;
- pJS.canvas.ctx.strokeStyle = 'rgba(' + color_line.r + ',' + color_line.g + ',' + color_line.b + ',' + opacity_line + ')';
- pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width;
- //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */
-
- /* path */
- pJS.canvas.ctx.beginPath();
- pJS.canvas.ctx.moveTo(p1.x, p1.y);
- pJS.canvas.ctx.lineTo(p2.x, p2.y);
- pJS.canvas.ctx.stroke();
- pJS.canvas.ctx.closePath();
-
- }
-
- }
-
- };
-
-
- pJS.fn.interact.attractParticles = function (p1, p2) {
-
- /* condensed particles */
- var dx = p1.x - p2.x,
- dy = p1.y - p2.y,
- dist = Math.sqrt(dx * dx + dy * dy);
-
- if (dist <= pJS.particles.line_linked.distance) {
-
- var ax = dx / (pJS.particles.move.attract.rotateX * 1000),
- ay = dy / (pJS.particles.move.attract.rotateY * 1000);
-
- p1.vx -= ax;
- p1.vy -= ay;
-
- p2.vx += ax;
- p2.vy += ay;
-
- }
-
-
- }
-
-
- pJS.fn.interact.bounceParticles = function (p1, p2) {
-
- var dx = p1.x - p2.x,
- dy = p1.y - p2.y,
- dist = Math.sqrt(dx * dx + dy * dy),
- dist_p = p1.radius + p2.radius;
-
- if (dist <= dist_p) {
- p1.vx = -p1.vx;
- p1.vy = -p1.vy;
-
- p2.vx = -p2.vx;
- p2.vy = -p2.vy;
- }
-
- }
-
-
- /* ---------- pJS functions - modes events ------------ */
-
- pJS.fn.modes.pushParticles = function (nb, pos) {
-
- pJS.tmp.pushing = true;
-
- for (var i = 0; i < nb; i++) {
- pJS.particles.array.push(
- new pJS.fn.particle(
- pJS.particles.color,
- pJS.particles.opacity.value,
- {
- 'x': pos ? pos.pos_x : Math.random() * pJS.canvas.w,
- 'y': pos ? pos.pos_y : Math.random() * pJS.canvas.h
- }
- )
- )
- if (i == nb - 1) {
- if (!pJS.particles.move.enable) {
- pJS.fn.particlesDraw();
- }
- pJS.tmp.pushing = false;
- }
- }
-
- };
-
-
- pJS.fn.modes.removeParticles = function (nb) {
-
- pJS.particles.array.splice(0, nb);
- if (!pJS.particles.move.enable) {
- pJS.fn.particlesDraw();
- }
-
- };
-
-
- pJS.fn.modes.bubbleParticle = function (p) {
-
- /* on hover event */
- if (pJS.interactivity.events.onhover.enable && isInArray('bubble', pJS.interactivity.events.onhover.mode)) {
-
- var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
- dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
- dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse),
- ratio = 1 - dist_mouse / pJS.interactivity.modes.bubble.distance;
-
- function init() {
- p.opacity_bubble = p.opacity;
- p.radius_bubble = p.radius;
- }
-
- /* mousemove - check ratio */
- if (dist_mouse <= pJS.interactivity.modes.bubble.distance) {
-
- if (ratio >= 0 && pJS.interactivity.status == 'mousemove') {
-
- /* size */
- if (pJS.interactivity.modes.bubble.size != pJS.particles.size.value) {
-
- if (pJS.interactivity.modes.bubble.size > pJS.particles.size.value) {
- var size = p.radius + (pJS.interactivity.modes.bubble.size * ratio);
- if (size >= 0) {
- p.radius_bubble = size;
- }
- } else {
- var dif = p.radius - pJS.interactivity.modes.bubble.size,
- size = p.radius - (dif * ratio);
- if (size > 0) {
- p.radius_bubble = size;
- } else {
- p.radius_bubble = 0;
- }
- }
-
- }
-
- /* opacity */
- if (pJS.interactivity.modes.bubble.opacity != pJS.particles.opacity.value) {
-
- if (pJS.interactivity.modes.bubble.opacity > pJS.particles.opacity.value) {
- var opacity = pJS.interactivity.modes.bubble.opacity * ratio;
- if (opacity > p.opacity && opacity <= pJS.interactivity.modes.bubble.opacity) {
- p.opacity_bubble = opacity;
- }
- } else {
- var opacity = p.opacity - (pJS.particles.opacity.value - pJS.interactivity.modes.bubble.opacity) * ratio;
- if (opacity < p.opacity && opacity >= pJS.interactivity.modes.bubble.opacity) {
- p.opacity_bubble = opacity;
- }
- }
-
- }
-
- }
-
- } else {
- init();
- }
-
-
- /* mouseleave */
- if (pJS.interactivity.status == 'mouseleave') {
- init();
- }
-
- }
-
- /* on click event */
- else if (pJS.interactivity.events.onclick.enable && isInArray('bubble', pJS.interactivity.events.onclick.mode)) {
-
-
- if (pJS.tmp.bubble_clicking) {
- var dx_mouse = p.x - pJS.interactivity.mouse.click_pos_x,
- dy_mouse = p.y - pJS.interactivity.mouse.click_pos_y,
- dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse),
- time_spent = (new Date().getTime() - pJS.interactivity.mouse.click_time) / 1000;
-
- if (time_spent > pJS.interactivity.modes.bubble.duration) {
- pJS.tmp.bubble_duration_end = true;
- }
-
- if (time_spent > pJS.interactivity.modes.bubble.duration * 2) {
- pJS.tmp.bubble_clicking = false;
- pJS.tmp.bubble_duration_end = false;
- }
- }
-
-
- function process(bubble_param, particles_param, p_obj_bubble, p_obj, id) {
-
- if (bubble_param != particles_param) {
-
- if (!pJS.tmp.bubble_duration_end) {
- if (dist_mouse <= pJS.interactivity.modes.bubble.distance) {
- if (p_obj_bubble != undefined) var obj = p_obj_bubble;
- else var obj = p_obj;
- if (obj != bubble_param) {
- var value = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration);
- if (id == 'size') p.radius_bubble = value;
- if (id == 'opacity') p.opacity_bubble = value;
- }
- } else {
- if (id == 'size') p.radius_bubble = undefined;
- if (id == 'opacity') p.opacity_bubble = undefined;
- }
- } else {
- if (p_obj_bubble != undefined) {
- var value_tmp = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration),
- dif = bubble_param - value_tmp;
- value = bubble_param + dif;
- if (id == 'size') p.radius_bubble = value;
- if (id == 'opacity') p.opacity_bubble = value;
- }
- }
-
- }
-
- }
-
- if (pJS.tmp.bubble_clicking) {
- /* size */
- process(pJS.interactivity.modes.bubble.size, pJS.particles.size.value, p.radius_bubble, p.radius, 'size');
- /* opacity */
- process(pJS.interactivity.modes.bubble.opacity, pJS.particles.opacity.value, p.opacity_bubble, p.opacity, 'opacity');
- }
-
- }
-
- };
-
-
- pJS.fn.modes.repulseParticle = function (p) {
-
- if (pJS.interactivity.events.onhover.enable && isInArray('repulse', pJS.interactivity.events.onhover.mode) && pJS.interactivity.status == 'mousemove') {
-
- var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
- dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
- dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse);
-
- var normVec = {x: dx_mouse / dist_mouse, y: dy_mouse / dist_mouse},
- repulseRadius = pJS.interactivity.modes.repulse.distance,
- velocity = 100,
- repulseFactor = clamp((1 / repulseRadius) * (-1 * Math.pow(dist_mouse / repulseRadius, 2) + 1) * repulseRadius * velocity, 0, 50);
-
- var pos = {
- x: p.x + normVec.x * repulseFactor,
- y: p.y + normVec.y * repulseFactor
- }
-
- if (pJS.particles.move.out_mode == 'bounce') {
- if (pos.x - p.radius > 0 && pos.x + p.radius < pJS.canvas.w) p.x = pos.x;
- if (pos.y - p.radius > 0 && pos.y + p.radius < pJS.canvas.h) p.y = pos.y;
- } else {
- p.x = pos.x;
- p.y = pos.y;
- }
-
- } else if (pJS.interactivity.events.onclick.enable && isInArray('repulse', pJS.interactivity.events.onclick.mode)) {
-
- if (!pJS.tmp.repulse_finish) {
- pJS.tmp.repulse_count++;
- if (pJS.tmp.repulse_count == pJS.particles.array.length) {
- pJS.tmp.repulse_finish = true;
- }
- }
-
- if (pJS.tmp.repulse_clicking) {
-
- var repulseRadius = Math.pow(pJS.interactivity.modes.repulse.distance / 6, 3);
-
- var dx = pJS.interactivity.mouse.click_pos_x - p.x,
- dy = pJS.interactivity.mouse.click_pos_y - p.y,
- d = dx * dx + dy * dy;
-
- var force = -repulseRadius / d * 1;
-
- function process() {
-
- var f = Math.atan2(dy, dx);
- p.vx = force * Math.cos(f);
- p.vy = force * Math.sin(f);
-
- if (pJS.particles.move.out_mode == 'bounce') {
- var pos = {
- x: p.x + p.vx,
- y: p.y + p.vy
- }
- if (pos.x + p.radius > pJS.canvas.w) p.vx = -p.vx;
- else if (pos.x - p.radius < 0) p.vx = -p.vx;
- if (pos.y + p.radius > pJS.canvas.h) p.vy = -p.vy;
- else if (pos.y - p.radius < 0) p.vy = -p.vy;
- }
-
- }
-
- // default
- if (d <= repulseRadius) {
- process();
- }
-
- // bang - slow motion mode
- // if(!pJS.tmp.repulse_finish){
- // if(d <= repulseRadius){
- // process();
- // }
- // }else{
- // process();
- // }
-
-
- } else {
-
- if (pJS.tmp.repulse_clicking == false) {
-
- p.vx = p.vx_i;
- p.vy = p.vy_i;
-
- }
-
- }
-
- }
-
- }
-
-
- pJS.fn.modes.grabParticle = function (p) {
-
- if (pJS.interactivity.events.onhover.enable && pJS.interactivity.status == 'mousemove') {
-
- var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
- dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
- dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse);
-
- /* draw a line between the cursor and the particle if the distance between them is under the config distance */
- if (dist_mouse <= pJS.interactivity.modes.grab.distance) {
-
- var opacity_line = pJS.interactivity.modes.grab.line_linked.opacity - (dist_mouse / (1 / pJS.interactivity.modes.grab.line_linked.opacity)) / pJS.interactivity.modes.grab.distance;
-
- if (opacity_line > 0) {
-
- /* style */
- var color_line = pJS.particles.line_linked.color_rgb_line;
- pJS.canvas.ctx.strokeStyle = 'rgba(' + color_line.r + ',' + color_line.g + ',' + color_line.b + ',' + opacity_line + ')';
- pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width;
- //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */
-
- /* path */
- pJS.canvas.ctx.beginPath();
- pJS.canvas.ctx.moveTo(p.x, p.y);
- pJS.canvas.ctx.lineTo(pJS.interactivity.mouse.pos_x, pJS.interactivity.mouse.pos_y);
- pJS.canvas.ctx.stroke();
- pJS.canvas.ctx.closePath();
-
- }
-
- }
-
- }
-
- };
-
-
- /* ---------- pJS functions - vendors ------------ */
-
- pJS.fn.vendors.eventsListeners = function () {
-
- /* events target element */
- if (pJS.interactivity.detect_on == 'window') {
- pJS.interactivity.el = window;
- } else {
- pJS.interactivity.el = pJS.canvas.el;
- }
-
-
- /* detect mouse pos - on hover / click event */
- if (pJS.interactivity.events.onhover.enable || pJS.interactivity.events.onclick.enable) {
-
- /* el on mousemove */
- pJS.interactivity.el.addEventListener('mousemove', function (e) {
-
- if (pJS.interactivity.el == window) {
- var pos_x = e.clientX,
- pos_y = e.clientY;
- } else {
- var pos_x = e.offsetX || e.clientX,
- pos_y = e.offsetY || e.clientY;
- }
-
- pJS.interactivity.mouse.pos_x = pos_x;
- pJS.interactivity.mouse.pos_y = pos_y;
-
- if (pJS.tmp.retina) {
- pJS.interactivity.mouse.pos_x *= pJS.canvas.pxratio;
- pJS.interactivity.mouse.pos_y *= pJS.canvas.pxratio;
- }
-
- pJS.interactivity.status = 'mousemove';
-
- });
-
- /* el on onmouseleave */
- pJS.interactivity.el.addEventListener('mouseleave', function (e) {
-
- pJS.interactivity.mouse.pos_x = null;
- pJS.interactivity.mouse.pos_y = null;
- pJS.interactivity.status = 'mouseleave';
-
- });
-
- }
-
- /* on click event */
- if (pJS.interactivity.events.onclick.enable) {
-
- pJS.interactivity.el.addEventListener('click', function () {
-
- pJS.interactivity.mouse.click_pos_x = pJS.interactivity.mouse.pos_x;
- pJS.interactivity.mouse.click_pos_y = pJS.interactivity.mouse.pos_y;
- pJS.interactivity.mouse.click_time = new Date().getTime();
-
- if (pJS.interactivity.events.onclick.enable) {
-
- switch (pJS.interactivity.events.onclick.mode) {
-
- case 'push':
- if (pJS.particles.move.enable) {
- pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse);
- } else {
- if (pJS.interactivity.modes.push.particles_nb == 1) {
- pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse);
- } else if (pJS.interactivity.modes.push.particles_nb > 1) {
- pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb);
- }
- }
- break;
-
- case 'remove':
- pJS.fn.modes.removeParticles(pJS.interactivity.modes.remove.particles_nb);
- break;
-
- case 'bubble':
- pJS.tmp.bubble_clicking = true;
- break;
-
- case 'repulse':
- pJS.tmp.repulse_clicking = true;
- pJS.tmp.repulse_count = 0;
- pJS.tmp.repulse_finish = false;
- setTimeout(function () {
- pJS.tmp.repulse_clicking = false;
- }, pJS.interactivity.modes.repulse.duration * 1000)
- break;
-
- }
-
- }
-
- });
-
- }
-
-
- };
-
- pJS.fn.vendors.densityAutoParticles = function () {
-
- if (pJS.particles.number.density.enable) {
-
- /* calc area */
- var area = pJS.canvas.el.width * pJS.canvas.el.height / 1000;
- if (pJS.tmp.retina) {
- area = area / (pJS.canvas.pxratio * 2);
- }
-
- /* calc number of particles based on density area */
- var nb_particles = area * pJS.particles.number.value / pJS.particles.number.density.value_area;
-
- /* add or remove X particles */
- var missing_particles = pJS.particles.array.length - nb_particles;
- if (missing_particles < 0) pJS.fn.modes.pushParticles(Math.abs(missing_particles));
- else pJS.fn.modes.removeParticles(missing_particles);
-
- }
-
- };
-
-
- pJS.fn.vendors.checkOverlap = function (p1, position) {
- for (var i = 0; i < pJS.particles.array.length; i++) {
- var p2 = pJS.particles.array[i];
-
- var dx = p1.x - p2.x,
- dy = p1.y - p2.y,
- dist = Math.sqrt(dx * dx + dy * dy);
-
- if (dist <= p1.radius + p2.radius) {
- p1.x = position ? position.x : Math.random() * pJS.canvas.w;
- p1.y = position ? position.y : Math.random() * pJS.canvas.h;
- pJS.fn.vendors.checkOverlap(p1);
- }
- }
- };
-
-
- pJS.fn.vendors.createSvgImg = function (p) {
-
- /* set color to svg element */
- var svgXml = pJS.tmp.source_svg,
- rgbHex = /#([0-9A-F]{3,6})/gi,
- coloredSvgXml = svgXml.replace(rgbHex, function (m, r, g, b) {
- if (p.color.rgb) {
- var color_value = 'rgba(' + p.color.rgb.r + ',' + p.color.rgb.g + ',' + p.color.rgb.b + ',' + p.opacity + ')';
- } else {
- var color_value = 'hsla(' + p.color.hsl.h + ',' + p.color.hsl.s + '%,' + p.color.hsl.l + '%,' + p.opacity + ')';
- }
- return color_value;
- });
-
- /* prepare to create img with colored svg */
- var svg = new Blob([coloredSvgXml], {type: 'image/svg+xml;charset=utf-8'}),
- DOMURL = window.URL || window.webkitURL || window,
- url = DOMURL.createObjectURL(svg);
-
- /* create particle img obj */
- var img = new Image();
- img.addEventListener('load', function () {
- p.img.obj = img;
- p.img.loaded = true;
- DOMURL.revokeObjectURL(url);
- pJS.tmp.count_svg++;
- });
- img.src = url;
-
- };
-
-
- pJS.fn.vendors.destroypJS = function () {
- cancelAnimationFrame(pJS.fn.drawAnimFrame);
- canvas_el.remove();
- pJSDom = null;
- };
-
-
- pJS.fn.vendors.drawShape = function (c, startX, startY, sideLength, sideCountNumerator, sideCountDenominator) {
-
- // By Programming Thomas - https://programmingthomas.wordpress.com/2013/04/03/n-sided-shapes/
- var sideCount = sideCountNumerator * sideCountDenominator;
- var decimalSides = sideCountNumerator / sideCountDenominator;
- var interiorAngleDegrees = (180 * (decimalSides - 2)) / decimalSides;
- var interiorAngle = Math.PI - Math.PI * interiorAngleDegrees / 180; // convert to radians
- c.save();
- c.beginPath();
- c.translate(startX, startY);
- c.moveTo(0, 0);
- for (var i = 0; i < sideCount; i++) {
- c.lineTo(sideLength, 0);
- c.translate(sideLength, 0);
- c.rotate(interiorAngle);
- }
- //c.stroke();
- c.fill();
- c.restore();
-
- };
-
- pJS.fn.vendors.exportImg = function () {
- window.open(pJS.canvas.el.toDataURL('image/png'), '_blank');
- };
-
-
- pJS.fn.vendors.loadImg = function (type) {
-
- pJS.tmp.img_error = undefined;
-
- if (pJS.particles.shape.image.src != '') {
-
- if (type == 'svg') {
-
- var xhr = new XMLHttpRequest();
- xhr.open('GET', pJS.particles.shape.image.src);
- xhr.onreadystatechange = function (data) {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- pJS.tmp.source_svg = data.currentTarget.response;
- pJS.fn.vendors.checkBeforeDraw();
- } else {
- console.log('Error pJS - Image not found');
- pJS.tmp.img_error = true;
- }
- }
- }
- xhr.send();
-
- } else {
-
- var img = new Image();
- img.addEventListener('load', function () {
- pJS.tmp.img_obj = img;
- pJS.fn.vendors.checkBeforeDraw();
- });
- img.src = pJS.particles.shape.image.src;
-
- }
-
- } else {
- console.log('Error pJS - No image.src');
- pJS.tmp.img_error = true;
- }
-
- };
-
-
- pJS.fn.vendors.draw = function () {
-
- if (pJS.particles.shape.type == 'image') {
-
- if (pJS.tmp.img_type == 'svg') {
-
- if (pJS.tmp.count_svg >= pJS.particles.number.value) {
- pJS.fn.particlesDraw();
- if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
- else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
- } else {
- //console.log('still loading...');
- if (!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
- }
-
- } else {
-
- if (pJS.tmp.img_obj != undefined) {
- pJS.fn.particlesDraw();
- if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
- else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
- } else {
- if (!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
- }
-
- }
-
- } else {
- pJS.fn.particlesDraw();
- if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
- else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
- }
-
- };
-
-
- pJS.fn.vendors.checkBeforeDraw = function () {
-
- // if shape is image
- if (pJS.particles.shape.type == 'image') {
-
- if (pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg == undefined) {
- pJS.tmp.checkAnimFrame = requestAnimFrame(check);
- } else {
- //console.log('images loaded! cancel check');
- cancelRequestAnimFrame(pJS.tmp.checkAnimFrame);
- if (!pJS.tmp.img_error) {
- pJS.fn.vendors.init();
- pJS.fn.vendors.draw();
- }
-
- }
-
- } else {
- pJS.fn.vendors.init();
- pJS.fn.vendors.draw();
- }
-
- };
-
-
- pJS.fn.vendors.init = function () {
-
- /* init canvas + particles */
- pJS.fn.retinaInit();
- pJS.fn.canvasInit();
- pJS.fn.canvasSize();
- pJS.fn.canvasPaint();
- pJS.fn.particlesCreate();
- pJS.fn.vendors.densityAutoParticles();
-
- /* particles.line_linked - convert hex colors to rgb */
- pJS.particles.line_linked.color_rgb_line = hexToRgb(pJS.particles.line_linked.color);
-
- };
-
-
- pJS.fn.vendors.start = function () {
-
- if (isInArray('image', pJS.particles.shape.type)) {
- pJS.tmp.img_type = pJS.particles.shape.image.src.substr(pJS.particles.shape.image.src.length - 3);
- pJS.fn.vendors.loadImg(pJS.tmp.img_type);
- } else {
- pJS.fn.vendors.checkBeforeDraw();
- }
-
- };
-
-
- /* ---------- pJS - start ------------ */
-
-
- pJS.fn.vendors.eventsListeners();
-
- pJS.fn.vendors.start();
-
-
-};
-
-/* ---------- global functions - vendors ------------ */
-
-Object.deepExtend = function (destination, source) {
- for (var property in source) {
- if (source[property] && source[property].constructor &&
- source[property].constructor === Object) {
- destination[property] = destination[property] || {};
- arguments.callee(destination[property], source[property]);
- } else {
- destination[property] = source[property];
- }
- }
- return destination;
-};
-
-window.requestAnimFrame = (function () {
- return window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame ||
- function (callback) {
- window.setTimeout(callback, 1000 / 60);
- };
-})();
-
-window.cancelRequestAnimFrame = (function () {
- return window.cancelAnimationFrame ||
- window.webkitCancelRequestAnimationFrame ||
- window.mozCancelRequestAnimationFrame ||
- window.oCancelRequestAnimationFrame ||
- window.msCancelRequestAnimationFrame ||
- clearTimeout
-})();
-
-function hexToRgb(hex) {
- // By Tim Down - http://stackoverflow.com/a/5624139/3493650
- // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
- var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
- hex = hex.replace(shorthandRegex, function (m, r, g, b) {
- return r + r + g + g + b + b;
- });
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- return result ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- } : null;
-};
-
-function clamp(number, min, max) {
- return Math.min(Math.max(number, min), max);
-};
-
-function isInArray(value, array) {
- return array.indexOf(value) > -1;
-}
-
-
-/* ---------- particles.js functions - start ------------ */
-
-window.pJSDom = [];
-
-window.particlesJS = function (tag_id, params) {
-
- //console.log(params);
-
- /* no string id? so it's object params, and set the id with default id */
- if (typeof (tag_id) != 'string') {
- params = tag_id;
- tag_id = 'particles-js';
- }
-
- /* no id? set the id to default id */
- if (!tag_id) {
- tag_id = 'particles-js';
- }
-
- /* pJS elements */
- var pJS_tag = document.getElementById(tag_id),
- pJS_canvas_class = 'particles-js-canvas-el',
- exist_canvas = pJS_tag.getElementsByClassName(pJS_canvas_class);
-
- /* remove canvas if exists into the pJS target tag */
- if (exist_canvas.length) {
- while (exist_canvas.length > 0) {
- pJS_tag.removeChild(exist_canvas[0]);
- }
- }
-
- /* create canvas element */
- var canvas_el = document.createElement('canvas');
- canvas_el.className = pJS_canvas_class;
-
- /* set size canvas */
- canvas_el.style.width = "100%";
- canvas_el.style.height = "100%";
-
- /* append canvas */
- var canvas = document.getElementById(tag_id).appendChild(canvas_el);
-
- /* launch particle.js */
- if (canvas != null) {
- pJSDom.push(new pJS(tag_id, params));
- }
-
-};
-
-window.particlesJS.load = function (tag_id, path_config_json, callback) {
-
- /* load json config */
- var xhr = new XMLHttpRequest();
- xhr.open('GET', path_config_json);
- xhr.onreadystatechange = function (data) {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- var params = JSON.parse(data.currentTarget.response);
- window.particlesJS(tag_id, params);
- if (callback) callback();
- } else {
- console.log('Error pJS - XMLHttpRequest status: ' + xhr.status);
- console.log('Error pJS - File config not found');
- }
- }
- };
- xhr.send();
-
-};
\ No newline at end of file
diff --git a/knockoutwhistweb/public/javascripts/websocket.js b/knockoutwhistweb/public/javascripts/websocket.js
deleted file mode 100644
index d1aa607..0000000
--- a/knockoutwhistweb/public/javascripts/websocket.js
+++ /dev/null
@@ -1,192 +0,0 @@
-let ws = null;
-const pending = new Map();
-const handlers = new Map();
-
-let timer = null;
-
-function setupSocketHandlers(socket) {
- socket.onmessage = (event) => {
- console.debug("SERVER MESSAGE:", event.data);
- let msg;
- try {
- msg = JSON.parse(event.data);
- } catch (e) {
- console.debug("Non-JSON message from server:", event.data, e);
- return;
- }
-
- const id = msg.id;
- const eventType = msg.event;
- const status = msg.status;
- const data = msg.data;
-
- if (id && typeof status === "string") {
- const entry = pending.get(id);
- if (!entry) return;
- clearTimeout(entry.timer);
- pending.delete(id);
-
- if (status === "success") {
- entry.resolve(data === undefined ? {} : data);
- } else {
- entry.reject(new Error(msg.error || "Server returned error"));
- }
- return;
- }
-
- if (id && eventType) {
- const handler = handlers.get(eventType);
- const sendResponse = (result) => {
- const response = {id: id, event: eventType, status: result};
- if (socket && socket.readyState === WebSocket.OPEN) {
- socket.send(JSON.stringify(response));
- } else {
- console.warn("Cannot send response, websocket not open");
- }
- };
-
- if (!handler) {
- console.warn("No handler for event:", eventType);
- sendResponse({error: "No handler for event: " + eventType});
- return;
- }
-
- try {
- Promise.resolve(handler(data === undefined ? {} : data))
- .then(_ => sendResponse("success"))
- .catch(_ => sendResponse("error"));
- } catch (err) {
- sendResponse("error");
- }
- }
- };
-
- socket.onerror = (error) => {
- console.error("WebSocket Error:", error);
- if (timer) clearInterval(timer);
- for (const [id, entry] of pending.entries()) {
- clearTimeout(entry.timer);
- entry.reject(new Error("WebSocket error/closed"));
- pending.delete(id);
- }
- if (socket.readyState === WebSocket.OPEN) socket.close(1000, "Unexpected error.");
- };
-
- socket.onclose = (event) => {
- if (timer) clearInterval(timer);
- for (const [id, entry] of pending.entries()) {
- clearTimeout(entry.timer);
- entry.reject(new Error("WebSocket closed"));
- pending.delete(id);
- }
- if (event.wasClean) {
- console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
- } else {
- console.warn('Connection died unexpectedly.');
- }
- location.href = "/mainmenu";
- };
-}
-
-function connectWebSocket(url = null) {
- if (!url) {
- const loc = window.location;
- const protocol = loc.protocol === "https:" ? "wss:" : "ws:";
- url = protocol + "//" + loc.host + "/websocket";
- }
- if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
- if (ws && ws.readyState === WebSocket.CONNECTING) {
- return new Promise((resolve, reject) => {
- const prevOnOpen = ws.onopen;
- const prevOnError = ws.onerror;
- ws.onopen = (ev) => {
- if (prevOnOpen) prevOnOpen(ev);
- resolve();
- };
- ws.onerror = (err) => {
- if (prevOnError) prevOnError(err);
- reject(err);
- };
- });
- }
-
- ws = new WebSocket(url);
- setupSocketHandlers(ws);
-
- return new Promise((resolve, reject) => {
- ws.onopen = () => {
- console.log("WebSocket connection established!");
- timer = setInterval(() => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- sendEventAndWait("ping", {}).then(
- () => console.debug("PING RESPONSE RECEIVED"),
- ).catch(
- (err) => console.warn("PING ERROR:", err.message),
- );
- console.debug("PING SENT");
- }
- }, 5000);
- resolve();
- };
-
- ws.onerror = (err) => {
- reject(err);
- };
- });
-}
-
-function disconnectWebSocket(code = 1000, reason = "Client disconnect") {
- if (timer) {
- clearInterval(timer);
- timer = null;
- }
- if (ws) {
- try {
- ws.close(code, reason);
- } catch (e) {
- }
- ws = null;
- }
-}
-
-function sendEvent(eventType, eventData) {
- if (!ws || ws.readyState !== WebSocket.OPEN) {
- console.warn("WebSocket is not open. Unable to send message.");
- return;
- }
- const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
- const message = {id: id, event: eventType, data: eventData};
- ws.send(JSON.stringify(message));
- console.debug("SENT:", message);
-}
-
-function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
- if (!ws || ws.readyState !== WebSocket.OPEN) {
- return Promise.reject(new Error("WebSocket is not open"));
- }
- const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
- const message = {id: id, event: eventType, data: eventData};
- const p = new Promise((resolve, reject) => {
- const timerId = setTimeout(() => {
- if (pending.has(id)) {
- pending.delete(id);
- reject(new Error(`No response within ${timeoutMs}ms for id=${id}`));
- }
- }, timeoutMs);
- pending.set(id, {resolve, reject, timer: timerId});
- });
- ws.send(JSON.stringify(message));
- console.debug("SENT (await):", message);
- return p;
-}
-
-function onEvent(eventType, handler) {
- handlers.set(eventType, handler);
-}
-
-globalThis.sendEvent = sendEvent;
-globalThis.sendEventAndWait = sendEventAndWait;
-globalThis.onEvent = onEvent;
-globalThis.connectWebSocket = connectWebSocket;
-globalThis.disconnectWebSocket = disconnectWebSocket;
-globalThis.isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;