diff --git a/src/main/kotlin/de/pheerai/buzzer/alpine/attributes.kt b/src/main/kotlin/de/pheerai/buzzer/alpine/attributes.kt index a580a23..a462b11 100644 --- a/src/main/kotlin/de/pheerai/buzzer/alpine/attributes.kt +++ b/src/main/kotlin/de/pheerai/buzzer/alpine/attributes.kt @@ -43,3 +43,7 @@ fun HTMLTag.xText(text: String) { fun HTMLTag.xBind(type: String, data: String) { attributes["x-bind:$type"] = data } + +fun HTMLTag.xEffect(effect: String) { + attributes["x-effect"] = effect +} diff --git a/src/main/kotlin/de/pheerai/buzzer/clients/gameMasterClient.kt b/src/main/kotlin/de/pheerai/buzzer/clients/gameMasterClient.kt index 042d204..fbb2dfa 100644 --- a/src/main/kotlin/de/pheerai/buzzer/clients/gameMasterClient.kt +++ b/src/main/kotlin/de/pheerai/buzzer/clients/gameMasterClient.kt @@ -12,6 +12,11 @@ fun HTML.createGameMasterDocument() { rel = "stylesheet", type = "text/css" ) + link( + href = "/assets/gamemaster.css", + rel = "stylesheet", + type = "text/css" + ) script { src = "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer = true @@ -19,28 +24,61 @@ fun HTML.createGameMasterDocument() { script { src = "/assets/buzzerWebSocket.js" } } body { - xData("{receivedNames: [], buzzerWebSocket: null}") + xData("{receivedNames: [], buzzerWebSocket: null, firstPlayer: null }") xInit( //language=JavaScript """ () => { startWebSocket('/socket/master', (ev) => { + if (receivedNames.length === 0) { + firstPlayer = ev.data + } receivedNames.push(ev.data) }); } """.trimIndent() ) - div(classes = "parent") { - button { - xOn("click", "receivedNames = []") - +"Reset" + xEffect( + //language=JavaScript + """ + if (firstPlayer !== null) { + Alpine.data.buzzerWebSocket.send(`PLAYER:${"$"}{firstPlayer}`); + } else { + Alpine.data.buzzerWebSocket.send("CLEAR:"); } - ol { + """.trimIndent() + ) + div(classes = "parent") { + div(classes = "player") { + xShow("firstPlayer") + xText("firstPlayer") + } + div { xShow("!firstPlayer") } + div(classes = "playerlist") list@{ template { attributes["x-for"] = "name in receivedNames" - this@ol.li { xText("name") } + this@list.div(classes = "listedname") { xText("name") } } } + button(classes = "submit") { + xData("{clicked: false}") + xOn( + "click", + //language=JavaScript + """ + () => { + receivedNames = [] + firstPlayer = null + clicked = true + setTimeout(() => {clicked = false}, 50) + } + """.trimIndent() + ) + xOn("mousedown", "clicked = true") + xOn("touchstart", "clicked = true") + xBind("class", "{'active': clicked}") + +"Reset" + } } } } diff --git a/src/main/kotlin/de/pheerai/buzzer/clients/playerClient.kt b/src/main/kotlin/de/pheerai/buzzer/clients/playerClient.kt index 0fa491d..ea3bc3c 100644 --- a/src/main/kotlin/de/pheerai/buzzer/clients/playerClient.kt +++ b/src/main/kotlin/de/pheerai/buzzer/clients/playerClient.kt @@ -29,13 +29,23 @@ fun HTML.createPlayerDocument() { } } body { - xData("{playerName: \"\", buzzerWebSocket: null}") + xData("{playerName: \"\", buzzerWebSocket: null, firstPlayer: null}") div(classes = "parent") { + xEffect("console.log(firstPlayer)") xInit( //language=JavaScript """ () => { - startWebSocket('/socket/player') + startWebSocket('/socket/player', (ev) => { + console.log("received: " + ev.data) + if (ev.data === "CLEAR:") { + firstPlayer = null; + } else { + let newName = ev.data.split(":")[1] + console.log(newName) + setTimeout(() => { firstPlayer = newName }, 500) + } + }); } """.trimIndent() ) @@ -54,7 +64,7 @@ fun HTML.createPlayerDocument() { +"Enter a name first!" } button(classes = "submit") { - xShow("playerName") + xShow("playerName && !firstPlayer") xData("{clicked: false}") xOn( "click", @@ -78,6 +88,11 @@ fun HTML.createPlayerDocument() { } p { +"and I know this!" } } + div(classes = "lockindicator") { + xShow("firstPlayer") + xBind("class", "{'self': firstPlayer === playerName, 'other': firstPlayer !== playerName }") + xText("firstPlayer") + } } } } diff --git a/src/main/kotlin/de/pheerai/buzzer/handlers/handleGameMasterSocket.kt b/src/main/kotlin/de/pheerai/buzzer/handlers/handleGameMasterSocket.kt index 8c07644..8638beb 100644 --- a/src/main/kotlin/de/pheerai/buzzer/handlers/handleGameMasterSocket.kt +++ b/src/main/kotlin/de/pheerai/buzzer/handlers/handleGameMasterSocket.kt @@ -4,13 +4,15 @@ import de.pheerai.buzzer.data.GameMasterSocket import de.pheerai.buzzer.data.SessionStorage import io.ktor.http.cio.websocket.* import kotlinx.coroutines.isActive +import org.slf4j.Logger -suspend fun WebSocketSession.handleGameMasterSocket() { +suspend fun WebSocketSession.handleGameMasterSocket(log: Logger) { SessionStorage.gameMasterSessions.add(GameMasterSocket(this)) for (frame in incoming) { when (frame) { is Frame.Text -> { val text = frame.readText() + log.info("GM Socket received $text") SessionStorage.playerSessions.filter { it.session.isActive } .forEach { it.session.send(text) } } diff --git a/src/main/kotlin/de/pheerai/buzzer/routing/setupRoutes.kt b/src/main/kotlin/de/pheerai/buzzer/routing/setupRoutes.kt index 37fd54b..94ac00d 100644 --- a/src/main/kotlin/de/pheerai/buzzer/routing/setupRoutes.kt +++ b/src/main/kotlin/de/pheerai/buzzer/routing/setupRoutes.kt @@ -21,7 +21,7 @@ fun Application.configureRoutes() { private fun Routing.websocketRoutes(log: Logger) { webSocket("/socket/player") { handlePlayerSocket(log) } - webSocket("/socket/master") { handleGameMasterSocket() } + webSocket("/socket/master") { handleGameMasterSocket(log) } } private fun Routing.clientRoutes() { diff --git a/src/main/resources/css/gamemaster.css b/src/main/resources/css/gamemaster.css new file mode 100644 index 0000000..3a5bd6b --- /dev/null +++ b/src/main/resources/css/gamemaster.css @@ -0,0 +1,58 @@ +body, html { + height: 100%; + overflow: hidden; + font-size: var(--font-size); + box-sizing: border-box; + padding: var(--bumper-small-size); + margin: 0; + background-color: var(--background); + color: var(--text); +} + +div.parent { + display: grid; + grid-template-rows: 3fr auto 1fr; + height: 100%; + max-width: 100%; +} + +.player { + background-color: var(--foreground02); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + border-radius: var(--border-radius); +} + +.playerlist { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.listedname + .listedname::before { + content: "ยป"; + padding-left: var(--bumper-small-size); + padding-right: var(--bumper-small-size); +} + +.submit { + border-radius: var(--border-radius); + width: 100%; + max-width: 100%; + font-size: var(--font-size); + height: 100%; + border: solid 0.25vh var(--highlight-dark); + background-color: var(--foreground); + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.submit.active { + background-color: var(--highlight01); +} + diff --git a/src/main/resources/css/player.css b/src/main/resources/css/player.css index 52c6c0b..6f895cd 100644 --- a/src/main/resources/css/player.css +++ b/src/main/resources/css/player.css @@ -59,6 +59,26 @@ div > .submit { flex-direction: column; } -div > button.active { +div > .submit.active { background-color: var(--highlight01); } + +.lockindicator { + width: 100%; + max-width: 100%; + height: 100%; + border: solid 0.25vh var(--highlight-dark); + border-radius: var(--border-radius); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.lockindicator.self { + background-color: darkgreen; +} + +.lockindicator.other { + background-color: darkred; +}