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 -

-
-
- -
-
- Final Standings -
-
-
- 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) { -
-
- Return to Lobby -
-
- } 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) - -
-
-
-
-
-
-
-

Select Trump Suit

-
-
- @if(gamelobby.logic.getCurrentMatch.isDefined) { - @if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) { - - -
-
-
- @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 { - - } - } -
-
-
-
-
-
-
- \ 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) -
-
-
-
-
-
-
-

Tie Break

-
-
-
-

- 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 => - - -
-
- -
-
- -
-
- -
-
-
Currently Picked Cards
- -
- @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) { - @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) { -
-
-
-

@player

-
- @util.WebUIUtils.cardtoImage(card) -
-
-
-
- } - } else { -
- -
- } -
- } - } else { - - -
Currently Picked Cards
- -
- @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) { - @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) { -
-
-
-

@player

-
- @util.WebUIUtils.cardtoImage(card) -
-
-
-
- } - } else { -
- -
- } -
- } - -
-
-
-
-
-
-
- \ 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) -
-
-
- - -
-
- - -
-
- - -
- 2 - 3 - 4 - 5 - 6 - 7 -
-
-
-
Create Game
-
-
-
- \ 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]) - 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) - -
-
-
-
-

Game Rules Overview

-
- -
- - -
-
-

- -

-
-
- 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) -@alt@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: ` -
- -
- -
-
- - -
-
- -
- -
-
-
- `, - 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
-
TRICKS
-
- -
-
- -
- {{ 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
-
- - 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 }}

- -
-
Next Players
-
-

{{ name }}

-
-
-
- `, - 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: ` -
- - - - - - -
- -
-
-
-
- Lobby-Name: {{ lobbyName }} -
-
Exit
-
-
-
- -
-
-
- Players: {{ players.length }} / {{ maxPlayers }} -
-
-
- -
- - - - - -
-
-
- `, - - 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;